Merge branch 'develop' into pr/1217
This commit is contained in:
commit
88108880bb
|
@ -1,14 +1,16 @@
|
|||
# Welcome to the contributing guide for PeerTube
|
||||
|
||||
Interesting in contributing? Awesome!
|
||||
Interested in contributing? Awesome!
|
||||
|
||||
**Quick Links:**
|
||||
**This guide will present you the following contribution topics:**
|
||||
|
||||
* [Translate](#translate)
|
||||
* [Give your feedback](#give-your-feedback)
|
||||
* [Write documentation](#write-documentation)
|
||||
* [Develop](#develop)
|
||||
|
||||
* [Improve the website](#improve-the-website)
|
||||
* [Troubleshooting](#troubleshooting)
|
||||
* [Tutorials](#tutorials)
|
||||
|
||||
## Translate
|
||||
|
||||
|
@ -37,6 +39,15 @@ Some hints:
|
|||
* Models sent/received by the controllers are defined in [/shared/models](/shared/models) directory
|
||||
|
||||
|
||||
## Improve the website
|
||||
|
||||
PeerTube's website is [joinpeertube.org](https://joinpeertube.org), where people can learn about the project and how it works – note that it is not a PeerTube instance, but rather the project's homepage.
|
||||
|
||||
You can help us improve it too!
|
||||
|
||||
It is not hosted on GitHub but on [Framasoft](https://framasoft.org/)'s own [GitLab](https://about.gitlab.com/) instance, [FramaGit](https://framagit.org): https://framagit.org/framasoft/peertube/joinpeertube
|
||||
|
||||
|
||||
## Develop
|
||||
|
||||
Don't hesitate to talk about features you want to develop by creating/commenting an issue
|
||||
|
@ -122,7 +133,7 @@ and the web server is automatically restarted.
|
|||
$ npm run dev
|
||||
```
|
||||
|
||||
### Federation
|
||||
### Testing the federation of PeerTube servers
|
||||
|
||||
Create a PostgreSQL user **with the same name as your username** in order to avoid using the *postgres* user.
|
||||
Then, we can create the databases (if they don't already exist):
|
||||
|
@ -176,3 +187,11 @@ $ npm run mocha -- --exit --require ts-node/register/type-check --bail server/te
|
|||
|
||||
Instance configurations are in `config/test-{1,2,3,4,5,6}.yaml`.
|
||||
Note that only instance 2 has transcoding enabled.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
Please check out the issues and [list of common errors](https://docs.joinpeertube.org/lang/en/devdocs/troubleshooting.html).
|
||||
|
||||
### Tutorials
|
||||
|
||||
Please check out the related section in the [development documentation](https://docs.joinpeertube.org/lang/en/devdocs/index.html#tutorials). Contribute tutorials at [framagit.org/framasoft/peertube/documentation](https://framagit.org/framasoft/peertube/documentation).
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
/test4/
|
||||
/test5/
|
||||
/test6/
|
||||
/server/tests/fixtures/video_high_bitrate_1080p.mp4
|
||||
|
||||
# Production
|
||||
/storage/
|
||||
|
|
20
.travis.yml
20
.travis.yml
|
@ -14,7 +14,10 @@ addons:
|
|||
- g++-4.9
|
||||
postgresql: "9.4"
|
||||
|
||||
cache: yarn
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.cache/yarn
|
||||
- $HOME/fixtures
|
||||
|
||||
sudo: false
|
||||
|
||||
|
@ -39,17 +42,18 @@ matrix:
|
|||
- env: TEST_SUITE=api-1
|
||||
- env: TEST_SUITE=api-2
|
||||
- env: TEST_SUITE=api-3
|
||||
- env: TEST_SUITE=api-4
|
||||
- env: TEST_SUITE=cli
|
||||
- env: TEST_SUITE=lint
|
||||
- env: TEST_SUITE=jest
|
||||
|
||||
script:
|
||||
- travis_retry npm run travis -- "$TEST_SUITE"
|
||||
- NODE_PENDING_JOB_WAIT=1000 travis_retry npm run travis -- "$TEST_SUITE"
|
||||
|
||||
after_failure:
|
||||
- cat test1/logs/all-logs.log
|
||||
- cat test2/logs/all-logs.log
|
||||
- cat test3/logs/all-logs.log
|
||||
- cat test4/logs/all-logs.log
|
||||
- cat test5/logs/all-logs.log
|
||||
- cat test6/logs/all-logs.log
|
||||
- cat test1/logs/peertube.log
|
||||
- cat test2/logs/peertube.log
|
||||
- cat test3/logs/peertube.log
|
||||
- cat test4/logs/peertube.log
|
||||
- cat test5/logs/peertube.log
|
||||
- cat test6/logs/peertube.log
|
||||
|
|
|
@ -2,10 +2,16 @@
|
|||
|
||||
## Vocabulary
|
||||
|
||||
- **Fediverse:** several servers following each others.
|
||||
- **Fediverse:** several servers following one another, several users
|
||||
following each other. Designates federated communities in general.
|
||||
- **Vidiverse:** same as Fediverse, but federating videos specifically.
|
||||
- **Instance:** a server which runs PeerTube in the fediverse.
|
||||
- **Origin instance:** the instance on which the video was uploaded and which
|
||||
is seeding (through the WebSeed protocol) the video.
|
||||
- **Cache instance:** an instance that decided to make available a WebSeed
|
||||
of its own for a video originating from another instance. It sends a `ptCache`
|
||||
activity to notify the origin instance, which will then update its list of
|
||||
WebSeeds for the video.
|
||||
- **Following:** the action of a PeerTube instance which will follow another
|
||||
instance (subscribe to its videos).
|
||||
|
||||
|
@ -22,8 +28,8 @@
|
|||
* All the requests are retried several times if they fail.
|
||||
|
||||
### Instance
|
||||
* An instance has a websocket tracker which is responsible for all the video
|
||||
uploaded in it.
|
||||
* An instance has a websocket tracker which is responsible for all videos
|
||||
uploaded by its users.
|
||||
* An instance has an administrator that can follow other instances.
|
||||
* An instance can be configured to follow back automatically.
|
||||
* An instance can blacklist other instances (only used in "follow back"
|
||||
|
|
314
CHANGELOG.md
314
CHANGELOG.md
|
@ -1,5 +1,317 @@
|
|||
# Changelog
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
* **Docker:** Check you have all the storage fields in your `/config/production.yaml` file: https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/production.yaml#L34
|
||||
* **nginx:** Add redundancy endpoint in static file. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx**
|
||||
* **nginx:** Add socket io endpoint. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx**
|
||||
* Moderators can manage users now (add/delete/update/block)
|
||||
* Add `tmp` and `redundancy` directories in configuration file. **You should configure them in your production.yaml**
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Check free storage before upgrading in upgrade script ([@Nutomic](https://github.com/nutomic))
|
||||
* Explain that PeerTube must be stopped in prune storage script
|
||||
* Add some security directives in the systemd unit configuration file ([@rigelk](https://github.com/rigelk) & [@mkoppmann](https://github.com/mkoppmann))
|
||||
* Update FreeBSD startup script ([@gegeweb](https://github.com/gegeweb))
|
||||
|
||||
### Docker
|
||||
|
||||
* Patch docker entrypoint to speed up the chown at startup ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
|
||||
### Features
|
||||
|
||||
* Add Russian, Polish and Italian languages
|
||||
* Add user notifications:
|
||||
* Notification types:
|
||||
* Comment on my video
|
||||
* New video from my subscriptions
|
||||
* New video abuses (for moderators)
|
||||
* Blacklist/Unblacklist on my video
|
||||
* Video import finished (error or success)
|
||||
* Pending video published (after transcoding or a scheduled update)
|
||||
* My account or one of my channel has a new follower
|
||||
* Someone (except muted accounts) mentioned me in comments
|
||||
* A user registered on the instance (for moderators)
|
||||
* Notification actions:
|
||||
* Add a web notification
|
||||
* Send an english email
|
||||
* Add contact form in about page (**enabled by default**)
|
||||
* Add ability to unfederate a local video in blacklist modal (**checkbox checked by default**)
|
||||
* Support additional video extensions if transcoding is enabled (**enabled by default**)
|
||||
* Redirect to the last url on login
|
||||
* Add ability to automatically set the video caption in URL. Example: https://peertube2.cpy.re/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d?subtitle=ru
|
||||
* Automatically enable the last selected caption when watching a video
|
||||
* Add ability to disable, clear and list user videos history
|
||||
* Add a button to help to translate peertube
|
||||
* Add text in the report modal to explain to whom the report will be sent
|
||||
* Open my account menu entries on hover
|
||||
* Explain what features are enabled on the instance in the about page
|
||||
* Add an error message in the forgot password modal if the instance email system is not configured
|
||||
* Add sitemap
|
||||
* Add well known url to change password ([@rigelk](https://github.com/rigelk))
|
||||
* Remove 8GB video upload limit on client side. There may still be such limit depending on the reverse proxy configuration ([@scanlime](https://github.com/scanlime))
|
||||
* Add CSP ([@rigelk](https://github.com/rigelk) & [@Nutomic](https://github.com/nutomic))
|
||||
* Update title and description HTML tags when rendering video HTML page
|
||||
* Add webfinger support for remote follows ([@acid-chicken](https://github.com/acid-chicken))
|
||||
* Add tooltip to explain how the trending algorithm works ([@auberanger](https://github.com/auberanger))
|
||||
* Warn users when they want to delete a channel because they will not be able to create another channel with the same name
|
||||
* Warn users when they leave the video upload/update (on page refresh/tab close)
|
||||
* Set max user name, user display name, channel name and channel display name lengths to 50 characters ([@McFlat](https://github.com/mcflat))
|
||||
* Increase video abuse length to 3000 characters
|
||||
* Add totalLocalVideoFilesSize in the stats endpoint
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fix the addition of captions to a video
|
||||
* Fix federation of some videos
|
||||
* Fix NSFW blur on search
|
||||
* Add error message when trying to upload .ass subtitles
|
||||
* Fix default homepage in the progressive web application
|
||||
* Don't crash on queue error
|
||||
* Fix EXDEV errors if you have multiple mount points
|
||||
* Fix broken audio in transcoding with some videos
|
||||
* Fix crash on getVideoFileStream issue
|
||||
* Fix followers search
|
||||
* Remove trailing `/` in CLI import script ([@HesioZ](https://github.com/HesioZ/))
|
||||
* Use origin video url in canonical tag
|
||||
* Fix captions in HTTP fallback
|
||||
* Automatically refresh remote actors to fix deleted remote actors that are still displayed on some instances
|
||||
* Add missing translations in video embed page
|
||||
* Fix some styling issues in dark mode
|
||||
* Fix transcoding issues with some videos
|
||||
* Fix Mac OS mkv/avi upload
|
||||
* Fix menu overflow on mobile
|
||||
* Fix ownership button icons ([@joshmorel](https://github.com/joshmorel))
|
||||
|
||||
|
||||
## v1.1.0
|
||||
|
||||
***Since v1.0.1***
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Improve REST API documentation: https://docs.joinpeertube.org/api.html ([@rigelk](https://github.com/rigelk))
|
||||
* Add basic ActivityPub documentation: https://docs.joinpeertube.org/lang/en/devdocs/federation.html ([@rigelk](https://github.com/rigelk))
|
||||
* Add CLI option to run PeerTube without client ([@rigelk](https://github.com/rigelk))
|
||||
* Add manpage to peertube CLI ([@rigelk](https://github.com/rigelk))
|
||||
* Make backups of files in optimize-old-videos script ([@Nutomic](https://github.com/nutomic))
|
||||
* Allow peertube-import-videos.ts CLI script to run concurrently ([@McFlat](https://github.com/mcflat))
|
||||
|
||||
### Scripts
|
||||
|
||||
* Use DB information from config/production.yaml in upgrade script ([@ldidry](https://github.com/ldidry))
|
||||
* Add REPL script ([@McFlat](https://github.com/mcflat))
|
||||
|
||||
### Docker
|
||||
|
||||
* Add search and import settings env settings env variables ([@kaiyou](https://github.com/kaiyou))
|
||||
* Add docker dev image ([@am97](https://github.com/am97))
|
||||
* Improve docker compose template ([@Nutomic](https://github.com/nutomic))
|
||||
* Add postfix image
|
||||
* Redirect HTTP -> HTTPS
|
||||
* Disable Træfik web UI
|
||||
|
||||
### Features
|
||||
|
||||
* Automatically resume videos if the user is logged in
|
||||
* Hide automatically the menu when the window is resized ([@BO41](https://github.com/BO41))
|
||||
* Remove confirm modal for JavaScript/CSS injection ([@scanlime](https://github.com/scanlime))
|
||||
* Set bitrate limits for transcoding ([@Nutomic](https://github.com/nutomic))
|
||||
* Add moderation tools in the account page
|
||||
* Add bulk actions in users table (Delete/Ban for now)
|
||||
* Add search filter in admin users table
|
||||
* Add search filter in admin following
|
||||
* Add search filter in admin followers
|
||||
* Add ability to list all local videos
|
||||
* Add ability for users to mute an account or an instance
|
||||
* Add ability for administrators to mute an account or an instance
|
||||
* Rename "News" category to "News & Politics" ([@daker](https://github.com/daker))
|
||||
* Add explicit error message when changing video ownership ([@lucas-dclrcq](https://github.com/lucas-dclrcq))
|
||||
* Improve description of the HTTP video import feature ([@rigelk](https://github.com/rigelk))
|
||||
* Set shorter keyframe interval for transcoding (2 seconds) ([@Nutomic](https://github.com/nutomic))
|
||||
* Add ability to disable webtorrent (as a user) ([@rigelk](https://github.com/rigelk))
|
||||
* Make abuse-delete clearer ([@barbeque](https://github.com/barbeque))
|
||||
* Adding minimum signup age conforming to ceiling GPDR age ([@rigelk](https://github.com/rigelk))
|
||||
* Feature/description support fields length 1000 ([@McFlat](https://github.com/mcflat))
|
||||
* Add background effect to activated menu entry
|
||||
* Improve video upload error handling
|
||||
* Improve message visibility on signup
|
||||
* Auto login user on signup if email verification is disabled
|
||||
* Speed up PeerTube startup (in particular the first one)
|
||||
* Delete invalid or deleted remote videos
|
||||
* Add ability to admin to set email as verified ([@joshmorel](https://github.com/joshmorel))
|
||||
* Add separators in user moderation dropdown
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* AP mimeType -> mediaType
|
||||
* PeerTube is not in beta anymore
|
||||
* PeerTube is not in alpha anymore :p
|
||||
* Fix optimize old videos script
|
||||
* Check follow constraints when getting a video
|
||||
* Fix application-config initialization in CLI tools ([Yetangitu](https://github.com/Yetangitu))
|
||||
* Fix video pixel format compatibility (using yuv420p) ([@rigelk](https://github.com/rigelk))
|
||||
* Fix video `state` AP context ([tcitworld](https://github.com/tcitworld))
|
||||
* Fix Linked Signature compatibility
|
||||
* Fix AP collections pagination
|
||||
* Fix too big thumbnails (when using URL import)
|
||||
* Do not host remote AP objects: use redirection instead
|
||||
* Fix video miniature with a long name
|
||||
* Fix video views inconsistencies inside the federation
|
||||
* Fix video embed in Wordpress Gutenberg
|
||||
* Fix video channel videos url when scrolling
|
||||
* Fix player progress bar/seeking when changing resolution
|
||||
* Fix search tab title with no search
|
||||
* Fix YouTube video import with some videos
|
||||
|
||||
***Since v1.1.0-rc.1***
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Fix AP infinite redirection
|
||||
* Fix trending page
|
||||
|
||||
|
||||
## v1.1.0-rc.1 (since v1.1.0-alpha.2)
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Improve REST API documentation: https://docs.joinpeertube.org/api.html ([@rigelk](https://github.com/rigelk))
|
||||
* Add basic ActivityPub documentation: https://docs.joinpeertube.org/lang/en/devdocs/federation.html ([@rigelk](https://github.com/rigelk))
|
||||
* Add CLI option to run PeerTube without client ([@rigelk](https://github.com/rigelk))
|
||||
* Add manpage to peertube CLI ([@rigelk](https://github.com/rigelk))
|
||||
* Make backups of files in optimize-old-videos script ([@Nutomic](https://github.com/nutomic))
|
||||
* Allow peertube-import-videos.ts CLI script to run concurrently ([@McFlat](https://github.com/mcflat))
|
||||
|
||||
### Docker
|
||||
|
||||
* Improve docker compose template ([@Nutomic](https://github.com/nutomic))
|
||||
* Add postfix image
|
||||
* Redirect HTTP -> HTTPS
|
||||
* Disable Træfik web UI
|
||||
* Add ability to set an array in `PEERTUBE_TRUST_PROXY` ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
|
||||
### Features
|
||||
|
||||
* Add background effect to activated menu entry
|
||||
* Improve video upload error handling
|
||||
* Improve message visibility on signup
|
||||
* Auto login user on signup if email verification is disabled
|
||||
* Speed up PeerTube startup (in particular the first one)
|
||||
* Delete invalid or deleted remote videos
|
||||
* Add ability to admin to set email as verified ([@joshmorel](https://github.com/joshmorel))
|
||||
* Add separators in user moderation dropdown
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Check follow constraints when getting a video
|
||||
* Fix application-config initialization in CLI tools ([Yetangitu](https://github.com/Yetangitu))
|
||||
* Fix video pixel format compatibility (using yuv420p) ([@rigelk](https://github.com/rigelk))
|
||||
* Fix video `state` AP context ([tcitworld](https://github.com/tcitworld))
|
||||
* Fix Linked Signature compatibility
|
||||
* Fix AP collections pagination
|
||||
* Fix too big thumbnails (when using URL import)
|
||||
* Do not host remote AP objects: use redirection instead
|
||||
* Fix video miniature with a long name
|
||||
* Fix video views inconsistencies inside the federation
|
||||
* Fix video embed in Wordpress Gutenberg
|
||||
* Fix video channel videos url when scrolling
|
||||
* Fix player progress bar/seeking when changing resolution
|
||||
* Fix search tab title with no search
|
||||
* Fix YouTube video import with some videos
|
||||
|
||||
|
||||
## v1.1.0-alpha.2 (since v1.1.0-alpha.1)
|
||||
|
||||
### Security/Maintenance/Federation
|
||||
|
||||
* Add HTTP Signature in addition to Linked Signature:
|
||||
* It's faster
|
||||
* Will allow us to use RSA Signature 2018 in the future without too much incompatibilities in the peertube federation
|
||||
|
||||
### Features
|
||||
|
||||
* Set shorter keyframe interval for transcoding (2 seconds) ([@Nutomic](https://github.com/nutomic))
|
||||
* Add ability to disable webtorrent (as a user) ([@rigelk](https://github.com/rigelk))
|
||||
* Make abuse-delete clearer ([@barbeque](https://github.com/barbeque))
|
||||
* Adding minimum signup age conforming to ceiling GPDR age ([@rigelk](https://github.com/rigelk))
|
||||
* Feature/description support fields length 1000 ([@McFlat](https://github.com/mcflat))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Scale bitrate linearly with FPS ([@Nutomic](https://github.com/nutomic))
|
||||
* AP mimeType -> mediaType
|
||||
* PeerTube is not in beta anymore
|
||||
* PeerTube is not in alpha anymore :p
|
||||
* Fix optimize old videos script
|
||||
|
||||
|
||||
## v1.0.1
|
||||
|
||||
### Security/Maintenance/Federation
|
||||
|
||||
* Add HTTP Signature in addition to Linked Signature:
|
||||
* It's faster
|
||||
* Will allow us to use RSA Signature 2018 in the future without too much incompatibilities in the peertube federation
|
||||
|
||||
|
||||
## v1.1.0-alpha.1
|
||||
|
||||
We released this alpha version because some admins/users need some moderation tools we implemented in recent weeks.
|
||||
This release could contain bugs. Don't expect a stable v1.1.0 until December :)
|
||||
|
||||
### Scripts
|
||||
|
||||
* Use DB information from config/production.yaml in upgrade script ([@ldidry](https://github.com/ldidry))
|
||||
* Add REPL script ([@McFlat](https://github.com/mcflat))
|
||||
|
||||
### Docker
|
||||
|
||||
* Add search and import settings env settings env variables ([@kaiyou](https://github.com/kaiyou))
|
||||
* Add docker dev image ([@am97](https://github.com/am97))
|
||||
|
||||
### Features
|
||||
|
||||
* Automatically resume videos if the user is logged in
|
||||
* Hide automatically the menu when the window is resized ([@BO41](https://github.com/BO41))
|
||||
* Remove confirm modal for JavaScript/CSS injection ([@scanlime](https://github.com/scanlime))
|
||||
* Set bitrate limits for transcoding ([@Nutomic](https://github.com/nutomic))
|
||||
* Add moderation tools in the account page
|
||||
* Add bulk actions in users table (Delete/Ban for now)
|
||||
* Add search filter in admin users table
|
||||
* Add search filter in admin following
|
||||
* Add search filter in admin followers
|
||||
* Add ability to list all local videos
|
||||
* Add ability for users to mute an account or an instance
|
||||
* Add ability for administrators to mute an account or an instance
|
||||
* Rename "News" category to "News & Politics" ([@daker](https://github.com/daker))
|
||||
* Add explicit error message when changing video ownership ([@lucas-dclrcq](https://github.com/lucas-dclrcq))
|
||||
* Improve description of the HTTP video import feature ([@rigelk](https://github.com/rigelk))
|
||||
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### SECURITY
|
||||
|
||||
* Add more headers to HTTP signature to avoid actor impersonation by replaying modified signed HTTP requests (thanks Thibaut Girka)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* Check video exists before extending expiration
|
||||
* Correctly delete redundancy files
|
||||
* Fix account URI in remote comment modal ([@rigelk](https://github.com/rigelk))
|
||||
* Fix avatar update
|
||||
* Avoid old issue regarding duplicated hosts in database
|
||||
|
||||
|
||||
## v1.0.0-rc.2
|
||||
|
||||
### Bug fixes
|
||||
|
@ -87,7 +399,7 @@
|
|||
|
||||
### Features
|
||||
|
||||
* Video redundancy system (experimental, see [the doc](/support/doc/redundancy.md))
|
||||
* Video redundancy system (experimental, see [the doc](https://docs.joinpeertube.org/lang/en/devdocs/architecture.html#redundancy-between-instances))
|
||||
* Add peertube script (see [the doc](/support/doc/tools.md#cli-wrapper)) ([@rigelk](https://github.com/rigelk))
|
||||
* Improve download modal ([@rigelk](https://github.com/rigelk))
|
||||
* Add redirect after login ([@BO41](https://github.com/BO41))
|
||||
|
|
63
CREDITS.md
63
CREDITS.md
|
@ -3,65 +3,82 @@
|
|||
* [Chocobozzz](https://github.com/Chocobozzz)
|
||||
* [rigelk](https://github.com/rigelk)
|
||||
* [gegeweb](https://github.com/gegeweb)
|
||||
* [Nutomic](https://github.com/Nutomic)
|
||||
* [Jorropo](https://github.com/Jorropo)
|
||||
* [BO41](https://github.com/BO41)
|
||||
* [joshmorel](https://github.com/joshmorel)
|
||||
* [buoyantair](https://github.com/buoyantair)
|
||||
* [bnjbvr](https://github.com/bnjbvr)
|
||||
* [DavidLibeau](https://github.com/DavidLibeau)
|
||||
* [jankeromnes](https://github.com/jankeromnes)
|
||||
* [JohnXLivingston](https://github.com/JohnXLivingston)
|
||||
* [kaiyou](https://github.com/kaiyou)
|
||||
* [McFlat](https://github.com/McFlat)
|
||||
* [DimitriGilbert](https://github.com/DimitriGilbert)
|
||||
* [floSoX](https://github.com/floSoX)
|
||||
* [Green-Star](https://github.com/Green-Star)
|
||||
* [joshmorel](https://github.com/joshmorel)
|
||||
* [thomaskuntzz](https://github.com/thomaskuntzz)
|
||||
* [rezonant](https://github.com/rezonant)
|
||||
* [kaiyou](https://github.com/kaiyou)
|
||||
* [Nutomic](https://github.com/Nutomic)
|
||||
* [JohnXLivingston](https://github.com/JohnXLivingston)
|
||||
* [okhin](https://github.com/okhin)
|
||||
* [fflorent](https://github.com/fflorent)
|
||||
* [ldidry](https://github.com/ldidry)
|
||||
* [okhin](https://github.com/okhin)
|
||||
* [daftaupe](https://github.com/daftaupe)
|
||||
* [LecygneNoir](https://github.com/LecygneNoir)
|
||||
* [fflorent](https://github.com/fflorent)
|
||||
* [dedesite](https://github.com/dedesite)
|
||||
* [Nautigsam](https://github.com/Nautigsam)
|
||||
* [BO41](https://github.com/BO41)
|
||||
* [daftaupe](https://github.com/daftaupe)
|
||||
* [scanlime](https://github.com/scanlime)
|
||||
* [tcitworld](https://github.com/tcitworld)
|
||||
* [am97](https://github.com/am97)
|
||||
* [dadall](https://github.com/dadall)
|
||||
* [jonathanraes](https://github.com/jonathanraes)
|
||||
* [LecygneNoir](https://github.com/LecygneNoir)
|
||||
* [anoadragon453](https://github.com/anoadragon453)
|
||||
* [rhaamo](https://github.com/rhaamo)
|
||||
* [mrflos](https://github.com/mrflos)
|
||||
* [jocelynj](https://github.com/jocelynj)
|
||||
* [lucas-dclrcq](https://github.com/lucas-dclrcq)
|
||||
* [lucaspontoexe](https://github.com/lucaspontoexe)
|
||||
* [flyingrub](https://github.com/flyingrub)
|
||||
* [tcitworld](https://github.com/tcitworld)
|
||||
* [SerCom-KC](https://github.com/SerCom-KC)
|
||||
* [valvin1](https://github.com/valvin1)
|
||||
* [am97](https://github.com/am97)
|
||||
* [taziden](https://github.com/taziden)
|
||||
* [sticmac](https://github.com/sticmac)
|
||||
* [barbeque](https://github.com/barbeque)
|
||||
* [luzpaz](https://github.com/luzpaz)
|
||||
* [acid-chicken](https://github.com/acid-chicken)
|
||||
* [louistio](https://github.com/louistio)
|
||||
* [qsypoq](https://github.com/qsypoq)
|
||||
* [daker](https://github.com/daker)
|
||||
* [xyproto](https://github.com/xyproto)
|
||||
* [Anton-Latukha](https://github.com/Anton-Latukha)
|
||||
* [noplanman](https://github.com/noplanman)
|
||||
* [auberanger](https://github.com/auberanger)
|
||||
* [austinheap](https://github.com/austinheap)
|
||||
* [benabbottnz](https://github.com/benabbottnz)
|
||||
* [ewft](https://github.com/ewft)
|
||||
* [bradsk88](https://github.com/bradsk88)
|
||||
* [WildYorkies](https://github.com/WildYorkies)
|
||||
* [Ealhad](https://github.com/Ealhad)
|
||||
* [clementbrizard](https://github.com/clementbrizard)
|
||||
* [DeeJayBro](https://github.com/DeeJayBro)
|
||||
* [Edznux](https://github.com/Edznux)
|
||||
* [ebrehault](https://github.com/ebrehault)
|
||||
* [DatBewar](https://github.com/DatBewar)
|
||||
* [ReK2Fernandez](https://github.com/ReK2Fernandez)
|
||||
* [Yetangitu](https://github.com/Yetangitu)
|
||||
* [grizio](https://github.com/grizio)
|
||||
* [Glandos](https://github.com/Glandos)
|
||||
* [lanodan](https://github.com/lanodan)
|
||||
* [HesioZ](https://github.com/HesioZ)
|
||||
* [jagannathBhat](https://github.com/jagannathBhat)
|
||||
* [jlebras](https://github.com/jlebras)
|
||||
* [alcalyn](https://github.com/alcalyn)
|
||||
* [mkody](https://github.com/mkody)
|
||||
* [lucas-dclrcq](https://github.com/lucas-dclrcq)
|
||||
* [pichouk](https://github.com/pichouk)
|
||||
* [zapashcanon](https://github.com/zapashcanon)
|
||||
* [mart-e](https://github.com/mart-e)
|
||||
* [scanlime](https://github.com/scanlime)
|
||||
* [0mp](https://github.com/0mp)
|
||||
* [mkoppmann](https://github.com/mkoppmann)
|
||||
* [1000i100](https://github.com/1000i100)
|
||||
* [roipoussiere](https://github.com/roipoussiere)
|
||||
* [zeograd](https://github.com/zeograd)
|
||||
* [PhieF](https://github.com/PhieF)
|
||||
* [Quenty31](https://github.com/Quenty31)
|
||||
|
@ -77,6 +94,7 @@
|
|||
* [imbsky](https://github.com/imbsky)
|
||||
* [ctlaltdefeat](https://github.com/ctlaltdefeat)
|
||||
* [jomo](https://github.com/jomo)
|
||||
* [lsde](https://github.com/lsde)
|
||||
* [memoryboxes](https://github.com/memoryboxes)
|
||||
* [norrist](https://github.com/norrist)
|
||||
* [osauzet](https://github.com/osauzet)
|
||||
|
@ -88,11 +106,13 @@
|
|||
|
||||
# Translations
|
||||
|
||||
* [abdhessuk](https://trad.framasoft.org/zanata/profile/view/abdhessuk)
|
||||
* [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24)
|
||||
* [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo)
|
||||
* [alice](https://trad.framasoft.org/zanata/profile/view/alice)
|
||||
* [anastasia](https://trad.framasoft.org/zanata/profile/view/anastasia)
|
||||
* [autom](https://trad.framasoft.org/zanata/profile/view/autom)
|
||||
* [balaji](https://trad.framasoft.org/zanata/profile/view/balaji)
|
||||
* [bristow](https://trad.framasoft.org/zanata/profile/view/bristow)
|
||||
* [butterflyoffire](https://trad.framasoft.org/zanata/profile/view/butterflyoffire)
|
||||
* [chocobozzz](https://trad.framasoft.org/zanata/profile/view/chocobozzz)
|
||||
|
@ -103,22 +123,30 @@
|
|||
* [ehsaan](https://trad.framasoft.org/zanata/profile/view/ehsaan)
|
||||
* [esoforte](https://trad.framasoft.org/zanata/profile/view/esoforte)
|
||||
* [fkohrt](https://trad.framasoft.org/zanata/profile/view/fkohrt)
|
||||
* [giqtaqisi](https://trad.framasoft.org/zanata/profile/view/giqtaqisi)
|
||||
* [goofy](https://trad.framasoft.org/zanata/profile/view/goofy)
|
||||
* [gorkaazk](https://trad.framasoft.org/zanata/profile/view/gorkaazk)
|
||||
* [gwendald](https://trad.framasoft.org/zanata/profile/view/gwendald)
|
||||
* [h3zjp](https://trad.framasoft.org/zanata/profile/view/h3zjp)
|
||||
* [jfblanc](https://trad.framasoft.org/zanata/profile/view/jfblanc)
|
||||
* [jhertel](https://trad.framasoft.org/zanata/profile/view/jhertel)
|
||||
* [jmf](https://trad.framasoft.org/zanata/profile/view/jmf)
|
||||
* [jorropo](https://trad.framasoft.org/zanata/profile/view/jorropo)
|
||||
* [kairozen](https://trad.framasoft.org/zanata/profile/view/kairozen)
|
||||
* [kedemferre](https://trad.framasoft.org/zanata/profile/view/kedemferre)
|
||||
* [kousha](https://trad.framasoft.org/zanata/profile/view/kousha)
|
||||
* [krkk](https://trad.framasoft.org/zanata/profile/view/krkk)
|
||||
* [landrok](https://trad.framasoft.org/zanata/profile/view/landrok)
|
||||
* [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48)
|
||||
* [m4sk1n](https://trad.framasoft.org/zanata/profile/view/m4sk1n)
|
||||
* [matograine](https://trad.framasoft.org/zanata/profile/view/matograine)
|
||||
* [medow](https://trad.framasoft.org/zanata/profile/view/medow)
|
||||
* [mhu](https://trad.framasoft.org/zanata/profile/view/mhu)
|
||||
* [midgard](https://trad.framasoft.org/zanata/profile/view/midgard)
|
||||
* [nbrucy](https://trad.framasoft.org/zanata/profile/view/nbrucy)
|
||||
* [nitai](https://trad.framasoft.org/zanata/profile/view/nitai)
|
||||
* [noncommutativegeo](https://trad.framasoft.org/zanata/profile/view/noncommutativegeo)
|
||||
* [nopsidy](https://trad.framasoft.org/zanata/profile/view/nopsidy)
|
||||
* [nvivant](https://trad.framasoft.org/zanata/profile/view/nvivant)
|
||||
* [osoitz](https://trad.framasoft.org/zanata/profile/view/osoitz)
|
||||
* [outloudvi](https://trad.framasoft.org/zanata/profile/view/outloudvi)
|
||||
|
@ -129,6 +157,7 @@
|
|||
* [s8321414](https://trad.framasoft.org/zanata/profile/view/s8321414)
|
||||
* [sato_ss](https://trad.framasoft.org/zanata/profile/view/sato_ss)
|
||||
* [sercom_kc](https://trad.framasoft.org/zanata/profile/view/sercom_kc)
|
||||
* [severo](https://trad.framasoft.org/zanata/profile/view/severo)
|
||||
* [silkevicious](https://trad.framasoft.org/zanata/profile/view/silkevicious)
|
||||
* [sosha](https://trad.framasoft.org/zanata/profile/view/sosha)
|
||||
* [spla](https://trad.framasoft.org/zanata/profile/view/spla)
|
||||
|
@ -139,11 +168,17 @@
|
|||
* [thibaultmartin](https://trad.framasoft.org/zanata/profile/view/thibaultmartin)
|
||||
* [tirifto](https://trad.framasoft.org/zanata/profile/view/tirifto)
|
||||
* [tuxayo](https://trad.framasoft.org/zanata/profile/view/tuxayo)
|
||||
* [unextro](https://trad.framasoft.org/zanata/profile/view/unextro)
|
||||
* [unzarida](https://trad.framasoft.org/zanata/profile/view/unzarida)
|
||||
* [vincent](https://trad.framasoft.org/zanata/profile/view/vincent)
|
||||
* [wanhua](https://trad.framasoft.org/zanata/profile/view/wanhua)
|
||||
* [xinayder](https://trad.framasoft.org/zanata/profile/view/xinayder)
|
||||
* [xosem](https://trad.framasoft.org/zanata/profile/view/xosem)
|
||||
* [zveryok](https://trad.framasoft.org/zanata/profile/view/zveryok)
|
||||
* [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo)
|
||||
* [autom](https://trad.framasoft.org/zanata/profile/view/autom)
|
||||
* [curupira](https://trad.framasoft.org/zanata/profile/view/curupira)
|
||||
* [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48)
|
||||
|
||||
|
||||
# Design
|
||||
|
|
44
FAQ.md
44
FAQ.md
|
@ -5,6 +5,7 @@
|
|||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
|
||||
|
||||
- [Why did you create PeerTube?](#why-did-you-create-peertube)
|
||||
- [I don't like the name "PeerTube"](#i-dont-like-the-name-peertube)
|
||||
- [If nobody watches a video, is it seeded?](#if-nobody-watches-a-video-is-it-seeded)
|
||||
- [What is WebSeed?](#what-is-webseed)
|
||||
|
@ -12,15 +13,40 @@
|
|||
- [Will an index of all the videos of servers you follow be too large for small servers?](#will-an-index-of-all-the-videos-of-servers-you-follow-be-too-large-for-small-servers)
|
||||
- [Which container formats can I use for the videos I want to upload?](#which-container-formats-can-i-use-for-the-videos-i-want-to-upload)
|
||||
- [I want to change my domain name, how can I do that?](#i-want-to-change-my-domain-name-how-can-i-do-that)
|
||||
- [Why do we have to put our Twitter username in PeerTube configuration?](#why-do-we-have-to-put-our-twitter-username-in-peertube-configuration)
|
||||
- [How video views are calculated?](#how-video-views-are-calculated)
|
||||
- [Should I have a big server to run PeerTube?](#should-i-have-a-big-server-to-run-peertube)
|
||||
- [Can I seed videos with my classic BitTorrent client (Transmission, rTorrent...)?](#can-i-seed-videos-with-my-classic-bittorrent-client-transmission-rtorrent)
|
||||
- [Why host on GitHub and Framagit?](#why-host-on-github-and-framagit)
|
||||
- [Are you going to use the Steem blockchain?](#are-you-going-to-use-the-steem-blockchain)
|
||||
- [Are you going to support advertisements?](#are-you-going-to-support-advertisements)
|
||||
- [What is "creation dynamic" and why not modify it?](#what-is-creation-dynamic-and-why-not-modify-it)
|
||||
- [I have found a security vulnerability in PeerTube. Where and how should I report it?](#i-have-found-a-security-vulnerability-in-peertube-where-and-how-should-i-report-it)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Why did you create PeerTube?
|
||||
|
||||
We can't build a FOSS video streaming alternative to YouTube, Dailymotion,
|
||||
Vimeo... with centralized software. One organization alone may not have
|
||||
enough money to pay for bandwidth and video storage of its servers.
|
||||
|
||||
Our stance is that only a decentralized network of servers can provide an
|
||||
acceptable answer to technical issues (bandwidth, transcoding expenses, etc.)
|
||||
and social answers (need for a particular moderation policy, preserving
|
||||
content, etc.).
|
||||
|
||||
While a paragraph is not enough to answer all these problems, PeerTube has
|
||||
very early prouded itself for using a contributory design, both for creating
|
||||
communities as federated nodes (as [Mastodon](https://joinmastodon.org/) for
|
||||
example), and for seeding videos (instances can seed each other's videos). But it's not
|
||||
enough because one video could become popular and overload the server. That is
|
||||
why we need to use a P2P protocol to limit the server load. Thanks to
|
||||
[WebTorrent](https://github.com/feross/webtorrent), we can use BitTorrent
|
||||
inside most modern web browsers, and users become seeds as the video gets
|
||||
more viewers.
|
||||
|
||||
|
||||
## I don't like the name "PeerTube"
|
||||
|
||||
PeerTube is just the name of the software. You can install it on your
|
||||
|
@ -32,7 +58,7 @@ is named "Framatube".
|
|||
|
||||
Yes, the origin server always seeds videos uploaded on it thanks to
|
||||
[Webseed](http://www.bittorrent.org/beps/bep_0019.html).
|
||||
It can also be helped by other servers using [redundancy](/support/doc/redundancy.md).
|
||||
It can also be helped by other servers using [redundancy](https://docs.joinpeertube.org/lang/en/devdocs/architecture.html#redundancy-between-instances).
|
||||
|
||||
|
||||
## What is WebSeed?
|
||||
|
@ -65,6 +91,18 @@ WEBM, MP4 or OGV videos.
|
|||
You can't. You'll need to reinstall an instance and reupload your videos.
|
||||
|
||||
|
||||
## Why do we have to put our Twitter username in PeerTube configuration?
|
||||
|
||||
You don't have to: we set a default value if you don't have a Twitter account.
|
||||
We need this information because Twitter requires an account for links share/videos embed on their platform.
|
||||
|
||||
|
||||
## How video views are calculated?
|
||||
|
||||
Your web browser sends a view to the server after 30 seconds of playback. Then, the IP cannot send another view in the next hour.
|
||||
Views are buffered, so don't panic if the view counter stays the same after you watched a video.
|
||||
|
||||
|
||||
## Should I have a big server to run PeerTube?
|
||||
|
||||
Not really. For instance, the demonstration server [https://peertube.cpy.re](https://peertube.cpy.re) has 2 vCore and 2GB of RAM and consumes on average:
|
||||
|
@ -120,3 +158,7 @@ If you still want to use a functionality potentially altering that state of thin
|
|||
|
||||
With that being said, know that we are not against these features *per se*.
|
||||
We are always open to discussion about potential PRs bringing in features, even of that kind. But we certainly won't dedicate our limited resources to develop them ourselves when there is so much to be done elsewhere.
|
||||
|
||||
## I have found a security vulnerability in PeerTube. Where and how should I report it?
|
||||
|
||||
We have a policy for contributions related to security. Please refer to [SECURITY.md](./SECURITY.md)
|
||||
|
|
15
SECURITY.md
15
SECURITY.md
|
@ -1,8 +1,6 @@
|
|||
**Introduction**
|
||||
|
||||
Security is core to our values, and we value the input of hackers acting in good faith to help us maintain a high standard for the security and privacy for our users. This includes encouraging responsible vulnerability research and disclosure. This policy sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return.
|
||||
|
||||
**Expectations**
|
||||
## Expectations
|
||||
|
||||
When working with us according to this policy, you can expect us to:
|
||||
- Extend Safe Harbor (see below) for your vulnerability research that is related to this policy;
|
||||
|
@ -10,7 +8,7 @@ When working with us according to this policy, you can expect us to:
|
|||
- Work to remediate discovered vulnerabilities in a timely manner; and
|
||||
- Recognize your contribution to improving our security if you are the first to report a unique vulnerability, and your report triggers a code or configuration change.
|
||||
|
||||
**Safe Harbor**
|
||||
## Safe Harbor
|
||||
|
||||
When conducting vulnerability research according to this policy, we consider this research to be:
|
||||
- Authorized in accordance with the law, and we will not initiate or support legal action against you for accidental, good faith violations of this policy;
|
||||
|
@ -22,7 +20,7 @@ You are expected, as always, to comply with all applicable laws.
|
|||
|
||||
If at any time you have concerns or are uncertain whether your security research is consistent with this policy, please submit a report through one of our Official Channels before going any further.
|
||||
|
||||
**Ground Rules**
|
||||
## Ground Rules
|
||||
|
||||
To encourage vulnerability research and to avoid any confusion between good-faith hacking and malicious attack, we ask that you:
|
||||
- Play by the rules. This includes following this policy, as well as any other relevant agreements. If there is any inconsistency between this policy and any other relevant terms, the terms of this policy will prevail.
|
||||
|
@ -35,10 +33,15 @@ To encourage vulnerability research and to avoid any confusion between good-fait
|
|||
- You should only interact with test accounts you own or with explicit permission from the account holder.
|
||||
- Do not engage in extortion.
|
||||
|
||||
**Official Channels**
|
||||
## Disclosure Terms
|
||||
|
||||
The vulnerability is kept private until a majority of instances known on instances.joinpeertube.org have updated to a safe version of PeerTube or applied a hotfix. The PeerTube development team coordinates efforts to update once the patch is issued.
|
||||
|
||||
## Official Channels
|
||||
|
||||
To help us receive vulnerability submissions we use the following official reporting channels:
|
||||
- chocobozzz@cpy.re (GPG: [583A612D890159BE](https://keybase.io/chocobozzz/pgp_keys.asc?fingerprint=c44aad638367912ca93edd57583a612d890159be))
|
||||
- sendmemail@rigelk.eu (GPG: [EA12971B0E438F36](https://api.github.com/users/rigelk/gpg_keys))
|
||||
|
||||
If you think you have found a vulnerability, please include the following details with your report and be as descriptive as possible:
|
||||
- The location and nature of the vulnerability,
|
||||
|
|
|
@ -23,7 +23,7 @@ export class VideoWatchPage {
|
|||
getVideosListName () {
|
||||
return element.all(by.css('.videos .video-miniature .video-miniature-name'))
|
||||
.getText()
|
||||
.then((texts: any) => texts.map(t => t.trim()))
|
||||
.then((texts: any) => texts.map((t: any) => t.trim()))
|
||||
}
|
||||
|
||||
waitWatchVideoName (videoName: string, isMobileDevice: boolean, isSafari: boolean) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "1.0.0-rc.2",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"licence": "GPLv3",
|
||||
"author": {
|
||||
|
@ -28,7 +28,8 @@
|
|||
"resolutions": {
|
||||
"video.js": "^7",
|
||||
"webtorrent/create-torrent/junk": "^1",
|
||||
"simple-get": "^2.8.1"
|
||||
"simple-get": "^2.8.1",
|
||||
"punycode": "^1.4.1"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -63,29 +64,31 @@
|
|||
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.8.3",
|
||||
"@angular/animations": "~6.1.4",
|
||||
"@angular/cli": "~6.2.3",
|
||||
"@angular/common": "~6.1.4",
|
||||
"@angular/compiler": "~6.1.4",
|
||||
"@angular/compiler-cli": "~6.1.4",
|
||||
"@angular/core": "~6.1.4",
|
||||
"@angular/forms": "~6.1.4",
|
||||
"@angular/http": "~6.1.4",
|
||||
"@angular/language-service": "~6.1.4",
|
||||
"@angular/platform-browser": "~6.1.4",
|
||||
"@angular/platform-browser-dynamic": "~6.1.4",
|
||||
"@angular/router": "~6.1.4",
|
||||
"@angular/service-worker": "~6.1.4",
|
||||
"@angular-devkit/build-angular": "~0.13.1",
|
||||
"@angular/animations": "~7.2.4",
|
||||
"@angular/cli": "~7.3.1",
|
||||
"@angular/common": "~7.2.4",
|
||||
"@angular/compiler": "~7.2.4",
|
||||
"@angular/compiler-cli": "~7.2.4",
|
||||
"@angular/core": "~7.2.4",
|
||||
"@angular/forms": "~7.2.4",
|
||||
"@angular/http": "~7.2.4",
|
||||
"@angular/language-service": "~7.2.4",
|
||||
"@angular/platform-browser": "~7.2.4",
|
||||
"@angular/platform-browser-dynamic": "~7.2.4",
|
||||
"@angular/router": "~7.2.4",
|
||||
"@angular/service-worker": "~7.2.4",
|
||||
"@angularclass/hmr": "^2.1.3",
|
||||
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^3.1.0",
|
||||
"@ngx-loading-bar/core": "^2.2.0",
|
||||
"@ngx-loading-bar/http-client": "^2.2.0",
|
||||
"@ngx-loading-bar/router": "^2.2.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^4.0.0",
|
||||
"@ngx-loading-bar/core": "^3.0.0",
|
||||
"@ngx-loading-bar/http-client": "^3.0.0",
|
||||
"@ngx-loading-bar/router": "^3.0.0",
|
||||
"@ngx-meta/core": "^6.0.0-rc.1",
|
||||
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
||||
"@streamroot/videojs-hlsjs-plugin": "^1.0.7",
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/hls.js": "^0.12.0",
|
||||
"@types/jasmine": "^2.8.7",
|
||||
"@types/jasminewd2": "^2.0.3",
|
||||
"@types/jest": "^23.3.1",
|
||||
|
@ -94,10 +97,10 @@
|
|||
"@types/markdown-it": "^0.0.5",
|
||||
"@types/node": "^10.9.2",
|
||||
"@types/sanitize-html": "1.18.0",
|
||||
"@types/video.js": "6.2.7",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/video.js": "^7.2.5",
|
||||
"@types/webtorrent": "^0.98.4",
|
||||
"angular2-hotkeys": "^2.1.2",
|
||||
"angular2-notifications": "^1.0.2",
|
||||
"awesome-typescript-loader": "5.2.1",
|
||||
"bootstrap": "^4.1.3",
|
||||
"buffer": "^5.1.0",
|
||||
|
@ -109,6 +112,7 @@
|
|||
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"focus-visible": "^4.1.5",
|
||||
"hls.js": "^0.12.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
|
@ -129,32 +133,33 @@
|
|||
"ngx-clipboard": "11.1.7",
|
||||
"ngx-pipes": "^2.1.7",
|
||||
"ngx-qrcode2": "^0.0.9",
|
||||
"ngx-textarea-autosize": "^2.0.0",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm-font-source-sans-pro": "^1.0.2",
|
||||
"p2p-media-loader-hlsjs": "^0.4.0",
|
||||
"path-browserify": "^1.0.0",
|
||||
"primeng": "^6.1.2",
|
||||
"primeng": "^7.0.0",
|
||||
"process": "^0.11.10",
|
||||
"protractor": "^5.3.2",
|
||||
"purify-css": "^1.2.5",
|
||||
"purifycss-webpack": "^0.7.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"rxjs": "^6.1.0",
|
||||
"rxjs": "^6.3.3",
|
||||
"sanitize-html": "^1.18.4",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sass-resources-loader": "^1.2.1",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"stream-browserify": "^2.0.1",
|
||||
"stream-http": "^2.8.3",
|
||||
"stream-http": "^3.0.0",
|
||||
"terser-webpack-plugin": "^1.1.0",
|
||||
"ts-jest": "^23.1.4",
|
||||
"tslint": "^5.7.0",
|
||||
"tslint-config-standard": "^8.0.1",
|
||||
"typescript": "2.9",
|
||||
"typescript": "3.1.6",
|
||||
"video.js": "^7",
|
||||
"videojs-contextmenu-ui": "^5.0.0",
|
||||
"videojs-contrib-quality-levels": "^2.0.9",
|
||||
"videojs-dock": "^2.0.2",
|
||||
"videojs-hotkeys": "^0.2.21",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-bundle-analyzer": "^3.0.2",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
|
||||
|
|
|
@ -1,39 +1,52 @@
|
|||
<div i18n class="about-instance-title">
|
||||
About {{ instanceName }} instance
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<div class="about-instance-title">
|
||||
<div i18n>About {{ instanceName }} instance</div>
|
||||
|
||||
<div class="short-description">
|
||||
<div>{{ shortDescription }}</div>
|
||||
</div>
|
||||
<div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
|
||||
</div>
|
||||
|
||||
<div class="description">
|
||||
<div i18n class="section-title">Description</div>
|
||||
<div class="short-description">
|
||||
<div>{{ shortDescription }}</div>
|
||||
</div>
|
||||
|
||||
<div [innerHTML]="descriptionHTML"></div>
|
||||
</div>
|
||||
<div class="description">
|
||||
<div i18n class="section-title">Description</div>
|
||||
|
||||
<div class="terms" id="terms-section">
|
||||
<div i18n class="section-title">Terms</div>
|
||||
<div [innerHTML]="descriptionHTML"></div>
|
||||
</div>
|
||||
|
||||
<div [innerHTML]="termsHTML"></div>
|
||||
</div>
|
||||
<div class="terms" id="terms-section">
|
||||
<div i18n class="section-title">Terms</div>
|
||||
|
||||
<div class="signup">
|
||||
<div i18n class="section-title">Signup</div>
|
||||
<div [innerHTML]="termsHTML"></div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isSignupAllowed">
|
||||
<ng-container i18n>User registration is allowed and</ng-container>
|
||||
<div class="signup">
|
||||
<div i18n class="section-title">Signup</div>
|
||||
|
||||
<ng-container i18n *ngIf="userVideoQuota !== -1">
|
||||
this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
|
||||
</ng-container>
|
||||
<div *ngIf="isSignupAllowed">
|
||||
<ng-container i18n>User registration is allowed and</ng-container>
|
||||
|
||||
<ng-container i18n *ngIf="userVideoQuota === -1">
|
||||
this instance provides unlimited space for the videos of its users.
|
||||
</ng-container>
|
||||
<ng-container i18n *ngIf="userVideoQuota !== -1">
|
||||
this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
|
||||
</ng-container>
|
||||
|
||||
<ng-container i18n *ngIf="userVideoQuota === -1">
|
||||
this instance provides unlimited space for the videos of its users.
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div i18n *ngIf="isSignupAllowed === false">
|
||||
User registration is currently not allowed.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div i18n *ngIf="isSignupAllowed === false">
|
||||
User registration is currently not allowed.
|
||||
<div class="col-md-12 col-xl-6">
|
||||
<label>Features found on this instance</label>
|
||||
<my-instance-features-table></my-instance-features-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>
|
||||
|
|
|
@ -2,9 +2,19 @@
|
|||
@import '_mixins';
|
||||
|
||||
.about-instance-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
& > div {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
& > .contact-admin {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
|
|
@ -1,23 +1,26 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ServerService } from '@app/core'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
|
||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about-instance',
|
||||
templateUrl: './about-instance.component.html',
|
||||
styleUrls: [ './about-instance.component.scss' ]
|
||||
})
|
||||
|
||||
export class AboutInstanceComponent implements OnInit {
|
||||
@ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
|
||||
|
||||
shortDescription = ''
|
||||
descriptionHTML = ''
|
||||
termsHTML = ''
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private serverService: ServerService,
|
||||
private instanceService: InstanceService,
|
||||
private markdownService: MarkdownService,
|
||||
private i18n: I18n
|
||||
) {}
|
||||
|
@ -34,8 +37,12 @@ export class AboutInstanceComponent implements OnInit {
|
|||
return this.serverService.getConfig().signup.allowed
|
||||
}
|
||||
|
||||
get isContactFormEnabled () {
|
||||
return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.serverService.getAbout()
|
||||
this.instanceService.getAbout()
|
||||
.subscribe(
|
||||
res => {
|
||||
this.shortDescription = res.instance.shortDescription
|
||||
|
@ -43,8 +50,12 @@ export class AboutInstanceComponent implements OnInit {
|
|||
this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms)
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error getting about from server'), err)
|
||||
() => this.notifier.error(this.i18n('Cannot get about information from server'))
|
||||
)
|
||||
}
|
||||
|
||||
openContactModal () {
|
||||
return this.contactAdminModal.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
|
||||
<div class="form-group">
|
||||
<label i18n for="fromName">Your name</label>
|
||||
<input
|
||||
type="text" id="fromName"
|
||||
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
|
||||
>
|
||||
<div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="fromEmail">Your email</label>
|
||||
<input
|
||||
type="text" id="fromEmail"
|
||||
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="body">Your message</label>
|
||||
<textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
|
||||
</textarea>
|
||||
<div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<span i18n class="action-button action-button-cancel" (click)="hide()">
|
||||
Cancel
|
||||
</span>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Submit" class="action-button-submit"
|
||||
[disabled]="!form.valid"
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
|
@ -0,0 +1,11 @@
|
|||
@import 'variables';
|
||||
@import 'mixins';
|
||||
|
||||
input[type=text] {
|
||||
@include peertube-input-text(340px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 200px);
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
||||
import { FormReactive, InstanceValidatorsService } from '@app/shared'
|
||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-contact-admin-modal',
|
||||
templateUrl: './contact-admin-modal.component.html',
|
||||
styleUrls: [ './contact-admin-modal.component.scss' ]
|
||||
})
|
||||
export class ContactAdminModalComponent extends FormReactive implements OnInit {
|
||||
@ViewChild('modal') modal: NgbModal
|
||||
|
||||
error: string
|
||||
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private modalService: NgbModal,
|
||||
private instanceValidatorsService: InstanceValidatorsService,
|
||||
private instanceService: InstanceService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get instanceName () {
|
||||
return this.serverService.getConfig().instance.name
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
fromName: this.instanceValidatorsService.FROM_NAME,
|
||||
fromEmail: this.instanceValidatorsService.FROM_EMAIL,
|
||||
body: this.instanceValidatorsService.BODY
|
||||
})
|
||||
}
|
||||
|
||||
show () {
|
||||
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
|
||||
}
|
||||
|
||||
hide () {
|
||||
this.form.reset()
|
||||
this.error = undefined
|
||||
|
||||
this.openedModal.close()
|
||||
this.openedModal = null
|
||||
}
|
||||
|
||||
sendForm () {
|
||||
const fromName = this.form.value['fromName']
|
||||
const fromEmail = this.form.value[ 'fromEmail' ]
|
||||
const body = this.form.value[ 'body' ]
|
||||
|
||||
this.instanceService.contactAdministrator(fromEmail, fromName, body)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Your message has been sent.'))
|
||||
this.hide()
|
||||
},
|
||||
|
||||
err => {
|
||||
this.error = err.status === 403
|
||||
? this.i18n('You already sent this form recently')
|
||||
: err.message
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -83,7 +83,7 @@
|
|||
<h6 i18n class="p2p-privacy-title">What will be done to mitigate this problem?</h6>
|
||||
|
||||
<p i18n>
|
||||
PeerTube is only in beta, and want to deliver the best countermeasures possible by the time the stable is released.
|
||||
PeerTube is in its early stages, and want to deliver the best countermeasures possible by the time the stable is released.
|
||||
In the meantime, we want to test different ideas related to this issue:
|
||||
</p>
|
||||
|
||||
|
@ -94,4 +94,4 @@
|
|||
<li i18n>Disable P2P from the administration interface</li>
|
||||
<li i18n>An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
|
|||
import { SharedModule } from '../shared'
|
||||
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
|
||||
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
|
||||
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
|
|||
declarations: [
|
||||
AboutComponent,
|
||||
AboutInstanceComponent,
|
||||
AboutPeertubeComponent
|
||||
AboutPeertubeComponent,
|
||||
ContactAdminModalComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit, OnDestroy } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
import { AccountService } from '@app/shared/account/account.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-about',
|
||||
|
|
|
@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
|
|||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Location } from '@angular/common'
|
||||
import { immutableAssign } from '@app/shared/misc/utils'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService } from '../../core/auth'
|
||||
import { ConfirmService } from '../../core/confirm'
|
||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||
|
@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators'
|
|||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
import { Notifier } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-videos',
|
||||
|
@ -35,7 +35,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
|
|||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected authService: AuthService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected notifier: Notifier,
|
||||
protected confirmService: ConfirmService,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
|
|
|
@ -10,8 +10,15 @@
|
|||
<div class="actor-name">{{ account.nameWithHost }}</div>
|
||||
|
||||
<span *ngIf="user?.blocked" [ngbTooltip]="user.blockedReason" class="badge badge-danger" i18n>Banned</span>
|
||||
<span *ngIf="account.mutedByUser" class="badge badge-danger" i18n>Muted</span>
|
||||
<span *ngIf="account.mutedServerByUser" class="badge badge-danger" i18n>Muted by your instance</span>
|
||||
<span *ngIf="account.mutedByInstance" class="badge badge-danger" i18n>Instance muted</span>
|
||||
<span *ngIf="account.mutedServerByInstance" class="badge badge-danger" i18n>Instance muted by your instance</span>
|
||||
|
||||
<my-user-moderation-dropdown buttonSize="small" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()">
|
||||
<my-user-moderation-dropdown
|
||||
buttonSize="small" [account]="account" [user]="user"
|
||||
(userChanged)="onUserChanged()" (userDeleted)="onUserDeleted()"
|
||||
>
|
||||
</my-user-moderation-dropdown>
|
||||
</div>
|
||||
<div i18n class="actor-followers">{{ account.followersCount }} subscribers</div>
|
||||
|
|
|
@ -5,10 +5,9 @@ import { Account } from '@app/shared/account/account.model'
|
|||
import { RestExtractor, UserService } from '@app/shared'
|
||||
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService, Notifier, RedirectService } from '@app/core'
|
||||
import { User, UserRight } from '../../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { AuthService, RedirectService } from '@app/core'
|
||||
|
||||
@Component({
|
||||
templateUrl: './accounts.component.html',
|
||||
|
@ -24,11 +23,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
private route: ActivatedRoute,
|
||||
private userService: UserService,
|
||||
private accountService: AccountService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private restExtractor: RestExtractor,
|
||||
private redirectService: RedirectService,
|
||||
private authService: AuthService,
|
||||
private i18n: I18n
|
||||
private authService: AuthService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -43,7 +41,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
.subscribe(
|
||||
account => this.account = account,
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -69,7 +67,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
.subscribe(
|
||||
user => this.user = user,
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,11 +10,12 @@ import { FollowingListComponent } from './follows/following-list/following-list.
|
|||
import { JobsComponent } from './jobs/job.component'
|
||||
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
||||
import { JobService } from './jobs/shared/job.service'
|
||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users'
|
||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
|
||||
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
||||
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -35,12 +36,15 @@ import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service
|
|||
UsersComponent,
|
||||
UserCreateComponent,
|
||||
UserUpdateComponent,
|
||||
UserPasswordComponent,
|
||||
UserListComponent,
|
||||
|
||||
ModerationComponent,
|
||||
VideoBlacklistListComponent,
|
||||
VideoAbuseListComponent,
|
||||
ModerationCommentModalComponent,
|
||||
InstanceServerBlocklistComponent,
|
||||
InstanceAccountBlocklistComponent,
|
||||
|
||||
JobsComponent,
|
||||
JobsListComponent,
|
||||
|
|
|
@ -7,161 +7,169 @@
|
|||
|
||||
<div i18n class="inner-form-title">Instance</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceName">Name</label>
|
||||
<input
|
||||
type="text" id="instanceName"
|
||||
formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.instanceName" class="form-error">
|
||||
{{ formErrors.instanceName }}
|
||||
<ng-container formGroupName="instance">
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceName">Name</label>
|
||||
<input
|
||||
type="text" id="instanceName"
|
||||
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
|
||||
>
|
||||
<div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceShortDescription">Short description</label>
|
||||
<textarea
|
||||
id="instanceShortDescription" formControlName="instanceShortDescription"
|
||||
[ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.instanceShortDescription" class="form-error">
|
||||
{{ formErrors.instanceShortDescription }}
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceShortDescription">Short description</label>
|
||||
<textarea
|
||||
id="instanceShortDescription" formControlName="shortDescription"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
|
||||
<my-markdown-textarea
|
||||
id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
|
||||
[classes]="{ 'input-error': formErrors['instanceDescription'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instanceDescription" class="form-error">
|
||||
{{ formErrors.instanceDescription }}
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
|
||||
<my-markdown-textarea
|
||||
id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
|
||||
[classes]="{ 'input-error': formErrors['instance.description'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
|
||||
<my-markdown-textarea
|
||||
id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
|
||||
[ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instanceTerms" class="form-error">
|
||||
{{ formErrors.instanceTerms }}
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
|
||||
<my-markdown-textarea
|
||||
id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.terms'] }"
|
||||
></my-markdown-textarea>
|
||||
<div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultClientRoute">Default client route</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
|
||||
<option i18n value="/videos/overview">Videos Overview</option>
|
||||
<option i18n value="/videos/trending">Videos Trending</option>
|
||||
<option i18n value="/videos/recently-added">Videos Recently Added</option>
|
||||
<option i18n value="/videos/local">Local videos</option>
|
||||
</select>
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultClientRoute">Default client route</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
|
||||
<option i18n value="/videos/overview">Videos Overview</option>
|
||||
<option i18n value="/videos/trending">Videos Trending</option>
|
||||
<option i18n value="/videos/recently-added">Videos Recently Added</option>
|
||||
<option i18n value="/videos/local">Local videos</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
|
||||
{{ formErrors.instanceDefaultClientRoute }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
|
||||
></my-help>
|
||||
<div class="form-group">
|
||||
<label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
|
||||
></my-help>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
|
||||
<option i18n value="do_not_list">Do not list</option>
|
||||
<option i18n value="blur">Blur thumbnails</option>
|
||||
<option i18n value="display">Display</option>
|
||||
</select>
|
||||
<div class="peertube-select-container">
|
||||
<select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
|
||||
<option i18n value="do_not_list">Do not list</option>
|
||||
<option i18n value="blur">Blur thumbnails</option>
|
||||
<option i18n value="display">Display</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
|
||||
{{ formErrors.instanceDefaultNSFWPolicy }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Signup</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="signupEnabled" formControlName="signupEnabled"
|
||||
i18n-labelText labelText="Signup enabled"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<my-peertube-checkbox *ngIf="isSignupEnabled()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<div *ngIf="isSignupEnabled()" class="form-group">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<input
|
||||
type="text" id="signupLimit"
|
||||
formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.signupLimit" class="form-error">
|
||||
{{ formErrors.signupLimit }}
|
||||
<ng-container formGroupName="signup">
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="signupEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Signup enabled"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div i18n class="inner-form-title">Import</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
|
||||
i18n-labelText labelText="Video import with HTTP enabled"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled"
|
||||
i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<div i18n class="inner-form-title">Administrator</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="adminEmail">Admin email</label>
|
||||
<input
|
||||
type="text" id="adminEmail"
|
||||
formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.adminEmail" class="form-error">
|
||||
{{ formErrors.adminEmail }}
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox *ngIf="isSignupEnabled()"
|
||||
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
|
||||
i18n-labelText labelText="Signup requires email verification"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isSignupEnabled()" class="form-group">
|
||||
<label i18n for="signupLimit">Signup limit</label>
|
||||
<input
|
||||
type="text" id="signupLimit"
|
||||
formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Users</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="userVideoQuota">User default video quota</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="userVideoQuota" formControlName="userVideoQuota">
|
||||
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
|
||||
{{ videoQuotaOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
<ng-container formGroupName="user">
|
||||
<div class="form-group">
|
||||
<label i18n for="userVideoQuota">User default video quota</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="userVideoQuota" formControlName="videoQuota">
|
||||
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
|
||||
{{ videoQuotaOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.userVideoQuota" class="form-error">
|
||||
{{ formErrors.userVideoQuota }}
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
|
||||
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
|
||||
{{ videoQuotaDailyOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Import</div>
|
||||
|
||||
<ng-container formGroupName="import">
|
||||
<ng-container formGroupName="videos">
|
||||
|
||||
<div class="form-group" formGroupName="http">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosHttpEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group" formGroupName="torrent">
|
||||
<my-peertube-checkbox
|
||||
inputName="importVideosTorrentEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Administrator</div>
|
||||
|
||||
<div class="form-group" formGroupName="admin">
|
||||
<label i18n for="adminEmail">Admin email</label>
|
||||
<input
|
||||
type="text" id="adminEmail"
|
||||
formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
|
||||
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
|
||||
{{ videoQuotaDailyOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
|
||||
{{ formErrors.userVideoQuotaDaily }}
|
||||
</div>
|
||||
<div class="form-group" formGroupName="contactForm">
|
||||
<my-peertube-checkbox
|
||||
inputName="enableContactForm" formControlName="enabled"
|
||||
i18n-labelText labelText="Enable contact form"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
|
||||
|
@ -169,28 +177,35 @@
|
|||
<ng-template ngbTabContent>
|
||||
<div i18n class="inner-form-title">Twitter</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="signupLimit">Your Twitter username</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="Indicates the Twitter account for the website or platform on which the content was published."
|
||||
></my-help>
|
||||
<input
|
||||
type="text" id="servicesTwitterUsername"
|
||||
formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
|
||||
{{ formErrors.servicesTwitterUsername }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container formGroupName="services">
|
||||
<ng-container formGroupName="twitter">
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="signupLimit">Your Twitter username</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="Indicates the Twitter account for the website or platform on which the content was published."
|
||||
></my-help>
|
||||
<input
|
||||
type="text" id="servicesTwitterUsername"
|
||||
formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
|
||||
i18n-labelText labelText="Instance whitelisted by Twitter"
|
||||
i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
|
||||
If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
|
||||
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
|
||||
i18n-labelText labelText="Instance whitelisted by Twitter"
|
||||
i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
|
||||
If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
|
||||
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
|
||||
></my-peertube-checkbox>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
|
||||
|
@ -199,36 +214,48 @@
|
|||
|
||||
<div i18n class="inner-form-title">Transcoding</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="transcodingEnabled" formControlName="transcodingEnabled"
|
||||
i18n-labelText labelText="Transcoding enabled"
|
||||
i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<ng-template [ngIf]="isTranscodingEnabled()">
|
||||
|
||||
<ng-container formGroupName="transcoding">
|
||||
<div class="form-group">
|
||||
<label i18n for="transcodingThreads">Transcoding threads</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="transcodingThreads" formControlName="transcodingThreads">
|
||||
<option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
|
||||
{{ transcodingThreadOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.transcodingThreads" class="form-error">
|
||||
{{ formErrors.transcodingThreads }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" *ngFor="let resolution of resolutions">
|
||||
<my-peertube-checkbox
|
||||
[inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)"
|
||||
i18n-labelText labelText="Resolution {{resolution}} enabled"
|
||||
inputName="transcodingEnabled" formControlName="enabled"
|
||||
i18n-labelText labelText="Transcoding enabled"
|
||||
i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-container *ngIf="isTranscodingEnabled()">
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
|
||||
i18n-labelText labelText="Allow additional extensions"
|
||||
i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="transcodingThreads">Transcoding threads</label>
|
||||
<div class="peertube-select-container">
|
||||
<select id="transcodingThreads" formControlName="threads">
|
||||
<option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
|
||||
{{ transcodingThreadOption.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
|
||||
</div>
|
||||
|
||||
<ng-container formGroupName="resolutions">
|
||||
<div class="form-group" *ngFor="let resolution of resolutions">
|
||||
<my-peertube-checkbox
|
||||
[inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
|
||||
i18n-labelText labelText="Resolution {{resolution}} enabled"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">
|
||||
Cache
|
||||
|
@ -239,74 +266,73 @@
|
|||
></my-help>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="cachePreviewsSize">Previews cache size</label>
|
||||
<input
|
||||
type="text" id="cachePreviewsSize"
|
||||
formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.cachePreviewsSize" class="form-error">
|
||||
{{ formErrors.cachePreviewsSize }}
|
||||
<ng-container formGroupName="cache">
|
||||
<div class="form-group" formGroupName="previews">
|
||||
<label i18n for="cachePreviewsSize">Previews cache size</label>
|
||||
<input
|
||||
type="text" id="cachePreviewsSize"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="cachePreviewsSize">Video captions cache size</label>
|
||||
<input
|
||||
type="text" id="cacheCaptionsSize"
|
||||
formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.cacheCaptionsSize" class="form-error">
|
||||
{{ formErrors.cacheCaptionsSize }}
|
||||
<div class="form-group" formGroupName="captions">
|
||||
<label i18n for="cacheCaptionsSize">Video captions cache size</label>
|
||||
<input
|
||||
type="text" id="cacheCaptionsSize"
|
||||
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
|
||||
>
|
||||
<div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<div i18n class="inner-form-title">Customizations</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n for="customizationJavascript">JavaScript</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
|
||||
></my-help>
|
||||
<textarea
|
||||
id="customizationJavascript" formControlName="customizationJavascript"
|
||||
[ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.customizationJavascript" class="form-error">
|
||||
{{ formErrors.customizationJavascript }}
|
||||
</div>
|
||||
</div>
|
||||
<ng-container formGroupName="instance">
|
||||
<ng-container formGroupName="customizations">
|
||||
<div class="form-group">
|
||||
<label i18n for="customizationJavascript">JavaScript</label>
|
||||
<my-help
|
||||
helpType="custom" i18n-customHtml
|
||||
customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
|
||||
></my-help>
|
||||
<textarea
|
||||
id="customizationJavascript" formControlName="javascript"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="customizationCSS">CSS</label>
|
||||
<my-help
|
||||
helpType="custom"
|
||||
i18n-customHtml
|
||||
customHtml="
|
||||
Write directly CSS code. Example:<br />
|
||||
<pre>
|
||||
body {{ '{' }}
|
||||
background-color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
<div class="form-group">
|
||||
<label for="customizationCSS">CSS</label>
|
||||
<my-help
|
||||
helpType="custom"
|
||||
i18n-customHtml
|
||||
customHtml="
|
||||
Write directly CSS code. Example:<br />
|
||||
<pre>
|
||||
body {{ '{' }}
|
||||
background-color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
|
||||
Prepend with <em>#custom-css</em> to override styles. Example:
|
||||
<pre>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
"
|
||||
></my-help>
|
||||
<textarea
|
||||
id="customizationCSS" formControlName="css"
|
||||
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
Prepend with <em>#custom-css</em> to override styles. Example:
|
||||
<pre>
|
||||
#custom-css .logged-in-email {{ '{' }}
|
||||
color: red;
|
||||
{{ '}' }}
|
||||
</pre>
|
||||
"
|
||||
></my-help>
|
||||
<textarea
|
||||
id="customizationCSS" formControlName="customizationCSS"
|
||||
[ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
|
||||
></textarea>
|
||||
<div *ngIf="formErrors.customizationCSS" class="form-error">
|
||||
{{ formErrors.customizationCSS }}
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
||||
import { ConfirmService } from '@app/core'
|
||||
import { ServerService } from '@app/core/server/server.service'
|
||||
import { CustomConfigValidatorsService, FormReactive, UserValidatorsService } from '@app/shared'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { CustomConfig } from '../../../../../../shared/models/server/custom-config.model'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { BuildFormDefaultValues, FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
|
@ -19,17 +18,13 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
resolutions: string[] = []
|
||||
transcodingThreadOptions: { label: string, value: number }[] = []
|
||||
|
||||
private oldCustomJavascript: string
|
||||
private oldCustomCSS: string
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private customConfigValidatorsService: CustomConfigValidatorsService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private configService: ConfigService,
|
||||
private serverService: ServerService,
|
||||
private confirmService: ConfirmService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
|
@ -60,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
}
|
||||
|
||||
getResolutionKey (resolution: string) {
|
||||
return 'transcodingResolution' + resolution
|
||||
return 'transcoding.resolutions.' + resolution
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
const formGroupData = {
|
||||
instanceName: this.customConfigValidatorsService.INSTANCE_NAME,
|
||||
instanceShortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
|
||||
instanceDescription: null,
|
||||
instanceTerms: null,
|
||||
instanceDefaultClientRoute: null,
|
||||
instanceDefaultNSFWPolicy: null,
|
||||
servicesTwitterUsername: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
|
||||
servicesTwitterWhitelisted: null,
|
||||
cachePreviewsSize: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE,
|
||||
cacheCaptionsSize: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE,
|
||||
signupEnabled: null,
|
||||
signupLimit: this.customConfigValidatorsService.SIGNUP_LIMIT,
|
||||
signupRequiresEmailVerification: null,
|
||||
importVideosHttpEnabled: null,
|
||||
importVideosTorrentEnabled: null,
|
||||
adminEmail: this.customConfigValidatorsService.ADMIN_EMAIL,
|
||||
userVideoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
|
||||
userVideoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY,
|
||||
transcodingThreads: this.customConfigValidatorsService.TRANSCODING_THREADS,
|
||||
transcodingEnabled: null,
|
||||
customizationJavascript: null,
|
||||
customizationCSS: null
|
||||
const formGroupData: { [key in keyof CustomConfig ]: any } = {
|
||||
instance: {
|
||||
name: this.customConfigValidatorsService.INSTANCE_NAME,
|
||||
shortDescription: this.customConfigValidatorsService.INSTANCE_SHORT_DESCRIPTION,
|
||||
description: null,
|
||||
terms: null,
|
||||
defaultClientRoute: null,
|
||||
defaultNSFWPolicy: null,
|
||||
customizations: {
|
||||
javascript: null,
|
||||
css: null
|
||||
}
|
||||
},
|
||||
services: {
|
||||
twitter: {
|
||||
username: this.customConfigValidatorsService.SERVICES_TWITTER_USERNAME,
|
||||
whitelisted: null
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: this.customConfigValidatorsService.CACHE_PREVIEWS_SIZE
|
||||
},
|
||||
captions: {
|
||||
size: this.customConfigValidatorsService.CACHE_CAPTIONS_SIZE
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: null,
|
||||
limit: this.customConfigValidatorsService.SIGNUP_LIMIT,
|
||||
requiresEmailVerification: null
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled: null
|
||||
},
|
||||
torrent: {
|
||||
enabled: null
|
||||
}
|
||||
}
|
||||
},
|
||||
admin: {
|
||||
email: this.customConfigValidatorsService.ADMIN_EMAIL
|
||||
},
|
||||
contactForm: {
|
||||
enabled: null
|
||||
},
|
||||
user: {
|
||||
videoQuota: this.userValidatorsService.USER_VIDEO_QUOTA,
|
||||
videoQuotaDaily: this.userValidatorsService.USER_VIDEO_QUOTA_DAILY
|
||||
},
|
||||
transcoding: {
|
||||
enabled: null,
|
||||
threads: this.customConfigValidatorsService.TRANSCODING_THREADS,
|
||||
allowAdditionalExtensions: null,
|
||||
resolutions: {}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultValues: BuildFormDefaultValues = {}
|
||||
const defaultValues = {
|
||||
transcoding: {
|
||||
resolutions: {}
|
||||
}
|
||||
}
|
||||
for (const resolution of this.resolutions) {
|
||||
const key = this.getResolutionKey(resolution)
|
||||
defaultValues[key] = 'false'
|
||||
formGroupData[key] = null
|
||||
defaultValues.transcoding.resolutions[resolution] = 'false'
|
||||
formGroupData.transcoding.resolutions[resolution] = null
|
||||
}
|
||||
|
||||
this.buildForm(formGroupData)
|
||||
|
@ -103,112 +136,25 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
res => {
|
||||
this.customConfig = res
|
||||
|
||||
this.oldCustomCSS = this.customConfig.instance.customizations.css
|
||||
this.oldCustomJavascript = this.customConfig.instance.customizations.javascript
|
||||
|
||||
this.updateForm()
|
||||
// Force form validation
|
||||
this.forceCheck()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
isTranscodingEnabled () {
|
||||
return this.form.value['transcodingEnabled'] === true
|
||||
return this.form.value['transcoding']['enabled'] === true
|
||||
}
|
||||
|
||||
isSignupEnabled () {
|
||||
return this.form.value['signupEnabled'] === true
|
||||
return this.form.value['signup']['enabled'] === true
|
||||
}
|
||||
|
||||
async formValidated () {
|
||||
const newCustomizationJavascript = this.form.value['customizationJavascript']
|
||||
const newCustomizationCSS = this.form.value['customizationCSS']
|
||||
|
||||
const customizations = []
|
||||
if (newCustomizationJavascript && newCustomizationJavascript !== this.oldCustomJavascript) customizations.push('JavaScript')
|
||||
if (newCustomizationCSS && newCustomizationCSS !== this.oldCustomCSS) customizations.push('CSS')
|
||||
|
||||
if (customizations.length !== 0) {
|
||||
const customizationsText = customizations.join('/')
|
||||
|
||||
// FIXME: i18n service does not support string concatenation
|
||||
const message = this.i18n('You set custom {{customizationsText}}. ', { customizationsText }) +
|
||||
this.i18n('This could lead to security issues or bugs if you do not understand it. ') +
|
||||
this.i18n('Are you sure you want to update the configuration?')
|
||||
|
||||
const label = this.i18n('Please type') + ` "I understand the ${customizationsText} I set" ` + this.i18n('to confirm.')
|
||||
const expectedInputValue = `I understand the ${customizationsText} I set`
|
||||
|
||||
const confirmRes = await this.confirmService.confirmWithInput(message, label, expectedInputValue)
|
||||
if (confirmRes === false) return
|
||||
}
|
||||
|
||||
const data: CustomConfig = {
|
||||
instance: {
|
||||
name: this.form.value['instanceName'],
|
||||
shortDescription: this.form.value['instanceShortDescription'],
|
||||
description: this.form.value['instanceDescription'],
|
||||
terms: this.form.value['instanceTerms'],
|
||||
defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
|
||||
defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
|
||||
customizations: {
|
||||
javascript: this.form.value['customizationJavascript'],
|
||||
css: this.form.value['customizationCSS']
|
||||
}
|
||||
},
|
||||
services: {
|
||||
twitter: {
|
||||
username: this.form.value['servicesTwitterUsername'],
|
||||
whitelisted: this.form.value['servicesTwitterWhitelisted']
|
||||
}
|
||||
},
|
||||
cache: {
|
||||
previews: {
|
||||
size: this.form.value['cachePreviewsSize']
|
||||
},
|
||||
captions: {
|
||||
size: this.form.value['cacheCaptionsSize']
|
||||
}
|
||||
},
|
||||
signup: {
|
||||
enabled: this.form.value['signupEnabled'],
|
||||
limit: this.form.value['signupLimit'],
|
||||
requiresEmailVerification: this.form.value['signupRequiresEmailVerification']
|
||||
},
|
||||
admin: {
|
||||
email: this.form.value['adminEmail']
|
||||
},
|
||||
user: {
|
||||
videoQuota: this.form.value['userVideoQuota'],
|
||||
videoQuotaDaily: this.form.value['userVideoQuotaDaily']
|
||||
},
|
||||
transcoding: {
|
||||
enabled: this.form.value['transcodingEnabled'],
|
||||
threads: this.form.value['transcodingThreads'],
|
||||
resolutions: {
|
||||
'240p': this.form.value[this.getResolutionKey('240p')],
|
||||
'360p': this.form.value[this.getResolutionKey('360p')],
|
||||
'480p': this.form.value[this.getResolutionKey('480p')],
|
||||
'720p': this.form.value[this.getResolutionKey('720p')],
|
||||
'1080p': this.form.value[this.getResolutionKey('1080p')]
|
||||
}
|
||||
},
|
||||
import: {
|
||||
videos: {
|
||||
http: {
|
||||
enabled: this.form.value['importVideosHttpEnabled']
|
||||
},
|
||||
torrent: {
|
||||
enabled: this.form.value['importVideosTorrentEnabled']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.configService.updateCustomConfig(data)
|
||||
this.configService.updateCustomConfig(this.form.value)
|
||||
.subscribe(
|
||||
res => {
|
||||
this.customConfig = res
|
||||
|
@ -218,45 +164,15 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
|
|||
|
||||
this.updateForm()
|
||||
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Configuration updated.'))
|
||||
this.notifier.success(this.i18n('Configuration updated.'))
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
private updateForm () {
|
||||
const data = {
|
||||
instanceName: this.customConfig.instance.name,
|
||||
instanceShortDescription: this.customConfig.instance.shortDescription,
|
||||
instanceDescription: this.customConfig.instance.description,
|
||||
instanceTerms: this.customConfig.instance.terms,
|
||||
instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
|
||||
instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
|
||||
servicesTwitterUsername: this.customConfig.services.twitter.username,
|
||||
servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
|
||||
cachePreviewsSize: this.customConfig.cache.previews.size,
|
||||
cacheCaptionsSize: this.customConfig.cache.captions.size,
|
||||
signupEnabled: this.customConfig.signup.enabled,
|
||||
signupLimit: this.customConfig.signup.limit,
|
||||
signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
|
||||
adminEmail: this.customConfig.admin.email,
|
||||
userVideoQuota: this.customConfig.user.videoQuota,
|
||||
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
|
||||
transcodingThreads: this.customConfig.transcoding.threads,
|
||||
transcodingEnabled: this.customConfig.transcoding.enabled,
|
||||
customizationJavascript: this.customConfig.instance.customizations.javascript,
|
||||
customizationCSS: this.customConfig.instance.customizations.css,
|
||||
importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
|
||||
importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
|
||||
}
|
||||
|
||||
for (const resolution of this.resolutions) {
|
||||
const key = this.getResolutionKey(resolution)
|
||||
data[key] = this.customConfig.transcoding.resolutions[resolution]
|
||||
}
|
||||
|
||||
this.form.patchValue(data)
|
||||
this.form.patchValue(this.customConfig)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,15 @@
|
|||
[value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<input
|
||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||
(keyup)="onSearch($event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n style="width: 60px">ID</th>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.caption {
|
||||
justify-content: flex-end;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { SortMeta } from 'primeng/primeng'
|
||||
import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
|
||||
import { RestPagination, RestTable } from '../../../shared'
|
||||
|
@ -20,7 +20,7 @@ export class FollowersListComponent extends RestTable implements OnInit {
|
|||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private followService: FollowService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -28,18 +28,18 @@ export class FollowersListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.followService.getFollowers(this.pagination, this.sort)
|
||||
this.followService.getFollowers(this.pagination, this.sort, this.search)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.followers = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { ConfirmService } from '../../../core'
|
||||
import { validateHost } from '../../../shared'
|
||||
import { FollowService } from '../shared'
|
||||
|
@ -18,7 +18,7 @@ export class FollowingAddComponent {
|
|||
|
||||
constructor (
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private followService: FollowService,
|
||||
private i18n: I18n
|
||||
|
@ -64,12 +64,12 @@ export class FollowingAddComponent {
|
|||
|
||||
this.followService.follow(hosts).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Follow request(s) sent!'))
|
||||
this.notifier.success(this.i18n('Follow request(s) sent!'))
|
||||
|
||||
setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,17 @@
|
|||
[value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div>
|
||||
<input
|
||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||
(keyup)="onSearch($event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n style="width: 60px">ID</th>
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
my-redundancy-checkbox /deep/ my-peertube-checkbox {
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.caption {
|
||||
justify-content: flex-end;
|
||||
|
||||
label {
|
||||
margin: 0;
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { SortMeta } from 'primeng/primeng'
|
||||
import { ActorFollow } from '../../../../../../shared/models/actors/follow.model'
|
||||
import { ConfirmService } from '../../../core/confirm/confirm.service'
|
||||
|
@ -20,7 +20,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
|
|||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private followService: FollowService,
|
||||
private i18n: I18n
|
||||
|
@ -29,7 +29,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
async removeFollowing (follow: ActorFollow) {
|
||||
|
@ -41,26 +41,23 @@ export class FollowingListComponent extends RestTable implements OnInit {
|
|||
|
||||
this.followService.unfollow(follow).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('You are not following {{host}} anymore.', { host: follow.following.host })
|
||||
)
|
||||
this.notifier.success(this.i18n('You are not following {{host}} anymore.', { host: follow.following.host }))
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.followService.getFollowing(this.pagination, this.sort)
|
||||
this.followService.getFollowing(this.pagination, this.sort, this.search)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.following = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ export class FollowService {
|
|||
) {
|
||||
}
|
||||
|
||||
getFollowing (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> {
|
||||
getFollowing (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (search) params = params.append('search', search)
|
||||
|
||||
return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/following', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
|
@ -29,10 +31,12 @@ export class FollowService {
|
|||
)
|
||||
}
|
||||
|
||||
getFollowers (pagination: RestPagination, sort: SortMeta): Observable<ResultList<ActorFollow>> {
|
||||
getFollowers (pagination: RestPagination, sort: SortMeta, search?: string): Observable<ResultList<ActorFollow>> {
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||
|
||||
if (search) params = params.append('search', search)
|
||||
|
||||
return this.authHttp.get<ResultList<ActorFollow>>(FollowService.BASE_APPLICATION_URL + '/followers', { params })
|
||||
.pipe(
|
||||
map(res => this.restExtractor.convertResultListDateToHuman(res)),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service'
|
||||
|
||||
|
@ -13,24 +13,21 @@ export class RedundancyCheckboxComponent {
|
|||
@Input() host: string
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private redundancyService: RedundancyService,
|
||||
private i18n: I18n
|
||||
) { }
|
||||
|
||||
updateRedundancyState () {
|
||||
this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed)
|
||||
.subscribe(
|
||||
() => {
|
||||
const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled')
|
||||
.subscribe(
|
||||
() => {
|
||||
const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled')
|
||||
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel })
|
||||
)
|
||||
},
|
||||
this.notifier.success(this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel }))
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { SortMeta } from 'primeng/primeng'
|
||||
import { Job } from '../../../../../../shared/index'
|
||||
import { JobState } from '../../../../../../shared/models'
|
||||
|
@ -25,7 +25,7 @@ export class JobsListComponent extends RestTable implements OnInit {
|
|||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private jobsService: JobService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -34,7 +34,7 @@ export class JobsListComponent extends RestTable implements OnInit {
|
|||
|
||||
ngOnInit () {
|
||||
this.loadJobState()
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
onJobStateChanged () {
|
||||
|
@ -53,7 +53,7 @@ export class JobsListComponent extends RestTable implements OnInit {
|
|||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './instance-account-blocklist.component'
|
||||
export * from './instance-server-blocklist.component'
|
|
@ -0,0 +1,22 @@
|
|||
<p-table
|
||||
[value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n>Account</th>
|
||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-accountBlock>
|
||||
<tr>
|
||||
<td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
|
||||
<td>{{ accountBlock.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -0,0 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.unblock-button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { RestPagination, RestTable } from '@app/shared'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-account-blocklist',
|
||||
styleUrls: [ './instance-account-blocklist.component.scss' ],
|
||||
templateUrl: './instance-account-blocklist.component.html'
|
||||
})
|
||||
export class InstanceAccountBlocklistComponent extends RestTable implements OnInit {
|
||||
blockedAccounts: AccountBlock[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private blocklistService: BlocklistService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
unblockAccount (accountBlock: AccountBlock) {
|
||||
const blockedAccount = accountBlock.blockedAccount
|
||||
|
||||
this.blocklistService.unblockAccountByInstance(blockedAccount)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
|
||||
)
|
||||
|
||||
this.loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.blocklistService.getInstanceAccountBlocklist(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.blockedAccounts = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<p-table
|
||||
[value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n>Instance</th>
|
||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-serverBlock>
|
||||
<tr>
|
||||
<td>{{ serverBlock.blockedServer.host }}</td>
|
||||
<td>{{ serverBlock.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -0,0 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.unblock-button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { RestPagination, RestTable } from '@app/shared'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { BlocklistService } from '@app/shared/blocklist'
|
||||
import { ServerBlock } from '../../../../../../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-instance-server-blocklist',
|
||||
styleUrls: [ './instance-server-blocklist.component.scss' ],
|
||||
templateUrl: './instance-server-blocklist.component.html'
|
||||
})
|
||||
export class InstanceServerBlocklistComponent extends RestTable implements OnInit {
|
||||
blockedServers: ServerBlock[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private blocklistService: BlocklistService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
unblockServer (serverBlock: ServerBlock) {
|
||||
const host = serverBlock.blockedServer.host
|
||||
|
||||
this.blocklistService.unblockServerByInstance(host)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
|
||||
|
||||
this.loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.blocklistService.getInstanceServerBlocklist(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.blockedServers = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -5,6 +5,10 @@
|
|||
<a *ngIf="hasVideoAbusesRight()" i18n routerLink="video-abuses/list" routerLinkActive="active">Video abuses</a>
|
||||
|
||||
<a *ngIf="hasVideoBlacklistRight()" i18n routerLink="video-blacklist/list" routerLinkActive="active">Blacklisted videos</a>
|
||||
|
||||
<a *ngIf="hasAccountsBlocklistRight()" i18n routerLink="blocklist/accounts" routerLinkActive="active">Muted accounts</a>
|
||||
|
||||
<a *ngIf="hasServersBlocklistRight()" i18n routerLink="blocklist/servers" routerLinkActive="active">Muted servers</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
font-weight: $font-semibold;
|
||||
min-width: 200px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.moderation-expanded-text {
|
||||
|
|
|
@ -16,4 +16,12 @@ export class ModerationComponent {
|
|||
hasVideoBlacklistRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)
|
||||
}
|
||||
|
||||
hasAccountsBlocklistRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_ACCOUNTS_BLOCKLIST)
|
||||
}
|
||||
|
||||
hasServersBlocklistRight () {
|
||||
return this.auth.getUser().hasRight(UserRight.MANAGE_SERVERS_BLOCKLIST)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { UserRightGuard } from '@app/core'
|
|||
import { VideoAbuseListComponent } from '@app/+admin/moderation/video-abuse-list'
|
||||
import { VideoBlacklistListComponent } from '@app/+admin/moderation/video-blacklist-list'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } from '@app/+admin/moderation/instance-blocklist'
|
||||
|
||||
export const ModerationRoutes: Routes = [
|
||||
{
|
||||
|
@ -46,6 +47,28 @@ export const ModerationRoutes: Routes = [
|
|||
title: 'Blacklisted videos'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blocklist/accounts',
|
||||
component: InstanceAccountBlocklistComponent,
|
||||
canActivate: [ UserRightGuard ],
|
||||
data: {
|
||||
userRight: UserRight.MANAGE_ACCOUNTS_BLOCKLIST,
|
||||
meta: {
|
||||
title: 'Muted accounts'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blocklist/servers',
|
||||
component: InstanceServerBlocklistComponent,
|
||||
canActivate: [ UserRightGuard ],
|
||||
data: {
|
||||
userRight: UserRight.MANAGE_SERVER_REDUNDANCY,
|
||||
meta: {
|
||||
title: 'Muted instances'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Moderation comment</h4>
|
||||
<span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
@ -14,12 +15,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div i18n>
|
||||
<div class="form-group" i18n>
|
||||
This comment can only be seen by you or the other moderators.
|
||||
</div>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span>
|
||||
<span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Update this comment" class="action-button-submit"
|
||||
|
@ -29,4 +30,4 @@
|
|||
</form>
|
||||
</div>
|
||||
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
|||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private modalService: NgbModal,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private videoAbuseService: VideoAbuseService,
|
||||
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
|
||||
private i18n: I18n
|
||||
|
@ -45,29 +45,26 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
|||
})
|
||||
}
|
||||
|
||||
hideModerationCommentModal () {
|
||||
hide () {
|
||||
this.abuseToComment = undefined
|
||||
this.openedModal.close()
|
||||
this.form.reset()
|
||||
}
|
||||
|
||||
async banUser () {
|
||||
const moderationComment: string = this.form.value['moderationComment']
|
||||
const moderationComment: string = this.form.value[ 'moderationComment' ]
|
||||
|
||||
this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment })
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Comment updated.')
|
||||
)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Comment updated.'))
|
||||
|
||||
this.commentUpdated.emit(moderationComment)
|
||||
this.hideModerationCommentModal()
|
||||
},
|
||||
this.commentUpdated.emit(moderationComment)
|
||||
this.hide()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th i18n>Video</th>
|
||||
<th i18n pSortableColumn="state" style="width: 80px;">State <p-sortIcon field="state"></p-sortIcon></th>
|
||||
<th style="width: 50px;"></th>
|
||||
<th style="width: 120px;"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
|||
</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
|
||||
<my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
@ -51,15 +51,15 @@
|
|||
<td class="moderation-expanded" colspan="6">
|
||||
<div>
|
||||
<span i18n class="moderation-expanded-label">Reason:</span>
|
||||
<span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
|
||||
</div>
|
||||
<div *ngIf="videoAbuse.moderationComment">
|
||||
<span i18n class="moderation-expanded-label">Moderation comment:</span>
|
||||
<span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
||||
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Account } from '../../../shared/account/account.model'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
|
||||
import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
|
||||
|
@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
|
|||
import { ConfirmService } from '../../../core/index'
|
||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||
import { Video } from '../../../shared/video/video.model'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-abuse-list',
|
||||
|
@ -27,16 +28,17 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
videoAbuseActions: DropdownAction<VideoAbuse>[] = []
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private videoAbuseService: VideoAbuseService,
|
||||
private confirmService: ConfirmService,
|
||||
private i18n: I18n
|
||||
private i18n: I18n,
|
||||
private markdownRenderer: MarkdownService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.videoAbuseActions = [
|
||||
{
|
||||
label: this.i18n('Delete'),
|
||||
label: this.i18n('Delete this report'),
|
||||
handler: videoAbuse => this.removeVideoAbuse(videoAbuse)
|
||||
},
|
||||
{
|
||||
|
@ -57,7 +59,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
openModerationCommentModal (videoAbuse: VideoAbuse) {
|
||||
|
@ -85,19 +87,16 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
async removeVideoAbuse (videoAbuse: VideoAbuse) {
|
||||
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete'))
|
||||
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse report?'), this.i18n('Delete'))
|
||||
if (res === false) return
|
||||
|
||||
this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Abuse deleted.')
|
||||
)
|
||||
this.notifier.success(this.i18n('Abuse deleted.'))
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -106,11 +105,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
.subscribe(
|
||||
() => this.loadData(),
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
|
@ -119,7 +122,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
<th style="width: 40px"></th>
|
||||
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th i18n>Sensitive</th>
|
||||
<th i18n>Unfederated</th>
|
||||
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th style="width: 50px;"></th>
|
||||
<th style="width: 120px;"></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
|
@ -26,20 +27,21 @@
|
|||
</a>
|
||||
</td>
|
||||
|
||||
<td>{{ videoBlacklist.video.nsfw }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||
<td>{{ videoBlacklist.createdAt }}</td>
|
||||
|
||||
<td class="action-cell">
|
||||
<my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
|
||||
<my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="rowexpansion" let-videoBlacklist>
|
||||
<tr>
|
||||
<td class="moderation-expanded" colspan="5">
|
||||
<td class="moderation-expanded" colspan="6">
|
||||
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
|
||||
<span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { ConfirmService } from '../../../core'
|
||||
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
|
||||
import { VideoBlacklist } from '../../../../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
|
||||
import { Video } from '../../../shared/video/video.model'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-blacklist-list',
|
||||
|
@ -23,9 +24,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private videoBlacklistService: VideoBlacklistService,
|
||||
private markdownRenderer: MarkdownService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
|
@ -39,13 +41,23 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
getVideoUrl (videoBlacklist: VideoBlacklist) {
|
||||
return Video.buildClientUrl(videoBlacklist.video.uuid)
|
||||
}
|
||||
|
||||
booleanToText (value: boolean) {
|
||||
if (value === true) return this.i18n('yes')
|
||||
|
||||
return this.i18n('no')
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||
}
|
||||
|
||||
async removeVideoFromBlacklist (entry: VideoBlacklist) {
|
||||
const confirmMessage = this.i18n(
|
||||
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
|
||||
|
@ -56,14 +68,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
|
||||
this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
|
||||
)
|
||||
this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name }))
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -75,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './user-create.component'
|
||||
export * from './user-update.component'
|
||||
export * from './user-password.component'
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { ServerService } from '../../../core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { UserCreate, UserRole } from '../../../../../../shared'
|
||||
import { UserEdit } from './user-edit'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
|
@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
|
|||
protected configService: ConfigService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
|
|||
|
||||
this.userService.addUser(userCreate).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('User {{username}} created.', { username: userCreate.username })
|
||||
)
|
||||
this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username }))
|
||||
this.router.navigate([ '/admin/users/list' ])
|
||||
},
|
||||
|
||||
|
|
|
@ -81,3 +81,17 @@
|
|||
|
||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
||||
<div *ngIf="!isCreation()" class="danger-zone">
|
||||
<div class="account-title" i18n>Danger Zone</div>
|
||||
|
||||
<div class="form-group reset-password-email">
|
||||
<label i18n>Send a link to reset the password by email to the user</label>
|
||||
<button (click)="resetPassword()" i18n>Ask for new password</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n>Manually set the user password</label>
|
||||
<my-user-password [userId]="userId"></my-user-password>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ input:not([type=submit]) {
|
|||
@include peertube-select-container(340px);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
input[type=submit], button {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
|
||||
|
@ -25,3 +25,23 @@ input[type=submit] {
|
|||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
@include in-content-small-title;
|
||||
|
||||
margin-top: 55px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
.reset-password-email {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
button {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import { ServerService } from '../../../core'
|
||||
import { FormReactive } from '../../../shared'
|
||||
import { USER_ROLE_LABELS, VideoResolution } from '../../../../../../shared'
|
||||
import { EditCustomConfigComponent } from '../../../+admin/config/edit-custom-config/'
|
||||
import { ConfigService } from '@app/+admin/config/shared/config.service'
|
||||
|
||||
export abstract class UserEdit extends FormReactive {
|
||||
|
||||
videoQuotaOptions: { value: string, label: string }[] = []
|
||||
videoQuotaDailyOptions: { value: string, label: string }[] = []
|
||||
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
|
||||
username: string
|
||||
userId: number
|
||||
|
||||
protected abstract serverService: ServerService
|
||||
protected abstract configService: ConfigService
|
||||
|
@ -23,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
|
|||
}
|
||||
|
||||
computeQuotaWithTranscoding () {
|
||||
const resolutions = this.serverService.getConfig().transcoding.enabledResolutions
|
||||
const transcodingConfig = this.serverService.getConfig().transcoding
|
||||
|
||||
const resolutions = transcodingConfig.enabledResolutions
|
||||
const higherResolution = VideoResolution.H_1080P
|
||||
let multiplier = 0
|
||||
|
||||
|
@ -31,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
|
|||
multiplier += resolution / higherResolution
|
||||
}
|
||||
|
||||
if (transcodingConfig.hls.enabled) multiplier *= 2
|
||||
|
||||
return multiplier * parseInt(this.form.value['videoQuota'], 10)
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
return
|
||||
}
|
||||
|
||||
protected buildQuotaOptions () {
|
||||
// These are used by a HTML select, so convert key into strings
|
||||
this.videoQuotaOptions = this.configService
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
|
||||
<div class="input-group">
|
||||
<input id="password" [attr.type]="showPassword ? 'text' : 'password'"
|
||||
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
|
||||
<ng-container *ngIf="!showPassword" i18n>Show</ng-container>
|
||||
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.password" class="form-error">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</form>
|
|
@ -0,0 +1,22 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
input:not([type=submit]):not([type=checkbox]) {
|
||||
@include peertube-input-text(340px);
|
||||
|
||||
display: block;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
height: 30px;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { UserService } from '@app/shared/users/user.service'
|
||||
import { Notifier } from '../../../core'
|
||||
import { User, UserUpdate } from '../../../../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
|
||||
import { FormReactive } from '../../../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-password',
|
||||
templateUrl: './user-password.component.html',
|
||||
styleUrls: [ './user-password.component.scss' ]
|
||||
})
|
||||
export class UserPasswordComponent extends FormReactive implements OnInit {
|
||||
error: string
|
||||
username: string
|
||||
showPassword = false
|
||||
|
||||
@Input() userId: number
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
password: this.userValidatorsService.USER_PASSWORD
|
||||
})
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.error = undefined
|
||||
|
||||
const userUpdate: UserUpdate = this.form.value
|
||||
|
||||
this.userService.updateUser(this.userId, userUpdate).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('Password changed for user {{username}}.', { username: this.username })
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
togglePasswordVisibility () {
|
||||
this.showPassword = !this.showPassword
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return this.i18n('Update user password')
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { ServerService } from '../../../core'
|
||||
import { UserEdit } from './user-edit'
|
||||
import { User, UserUpdate } from '../../../../../../shared'
|
||||
|
@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
|
|||
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||
error: string
|
||||
userId: number
|
||||
userEmail: string
|
||||
username: string
|
||||
|
||||
private paramsSub: Subscription
|
||||
|
@ -30,7 +31,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
|||
private userValidatorsService: UserValidatorsService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -73,10 +74,7 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
|||
|
||||
this.userService.updateUser(this.userId, userUpdate).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.i18n('User {{username}} updated.', { username: this.username })
|
||||
)
|
||||
this.notifier.success(this.i18n('User {{username}} updated.', { username: this.username }))
|
||||
this.router.navigate([ '/admin/users/list' ])
|
||||
},
|
||||
|
||||
|
@ -92,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
|||
return this.i18n('Update user')
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
this.userService.askResetPassword(this.userEmail).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
private onUserFetched (userJson: User) {
|
||||
this.userId = userJson.id
|
||||
this.username = userJson.username
|
||||
this.userEmail = userJson.email
|
||||
|
||||
this.form.patchValue({
|
||||
email: userJson.email,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div i18n class="form-sub-title">Users list</div>
|
||||
|
||||
<a class="add-button" routerLink="/admin/users/create">
|
||||
<span class="icon icon-add"></span>
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create user</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -10,9 +10,32 @@
|
|||
<p-table
|
||||
[value]="users" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" dataKey="id"
|
||||
[(selection)]="selectedUsers"
|
||||
>
|
||||
<ng-template pTemplate="caption">
|
||||
<div class="caption">
|
||||
<div>
|
||||
<my-action-dropdown
|
||||
*ngIf="isInSelectionMode()" i18n-label label="Batch actions" theme="orange"
|
||||
[actions]="bulkUserActions" [entry]="selectedUsers"
|
||||
>
|
||||
</my-action-dropdown>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="text" name="table-filter" id="table-filter" i18n-placeholder placeholder="Filter..."
|
||||
(keyup)="onSearch($event.target.value)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th style="width: 40px">
|
||||
<p-tableHeaderCheckbox></p-tableHeaderCheckbox>
|
||||
</th>
|
||||
<th style="width: 40px"></th>
|
||||
<th i18n pSortableColumn="username">Username <p-sortIcon field="username"></p-sortIcon></th>
|
||||
<th i18n>Email</th>
|
||||
|
@ -25,22 +48,42 @@
|
|||
|
||||
<ng-template pTemplate="body" let-expanded="expanded" let-user>
|
||||
|
||||
<tr [ngClass]="{ banned: user.blocked }">
|
||||
<tr [pSelectableRow]="user" [ngClass]="{ banned: user.blocked }">
|
||||
<td>
|
||||
<p-tableCheckbox [value]="user"></p-tableCheckbox>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span *ngIf="user.blockedReason" class="expander" [pRowToggler]="user">
|
||||
<i [ngClass]="expanded ? 'glyphicon glyphicon-menu-down' : 'glyphicon glyphicon-menu-right'"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ user.username }}
|
||||
<span *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||
<a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
|
||||
{{ user.username }}
|
||||
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ user.email }}</td>
|
||||
|
||||
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
|
||||
|
||||
<ng-template #emailWithVerificationStatus>
|
||||
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
|
||||
<em>? {{ user.email }}</em>
|
||||
</td>
|
||||
<ng-template #emailVerifiedNotFalse>
|
||||
<td i18n-title title="User's email is verified / User can login without email verification">
|
||||
✓ {{ user.email }}
|
||||
</td>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
||||
<td>{{ user.roleLabel }}</td>
|
||||
<td>{{ user.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<my-user-moderation-dropdown [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
|
||||
<my-user-moderation-dropdown *ngIf="!isInSelectionMode()" [user]="user" (userChanged)="onUserChanged()" (userDeleted)="onUserChanged()">
|
||||
</my-user-moderation-dropdown>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -56,3 +99,4 @@
|
|||
</ng-template>
|
||||
</p-table>
|
||||
|
||||
<my-user-ban-modal #userBanModal (userBanned)="onUserChanged()"></my-user-ban-modal>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import '_mixins';
|
||||
|
||||
.add-button {
|
||||
@include create-button('../../../../assets/images/global/add.svg');
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
tr.banned {
|
||||
|
@ -15,4 +15,12 @@ tr.banned {
|
|||
|
||||
.ban-reason-label {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
justify-content: space-between;
|
||||
|
||||
input {
|
||||
@include peertube-input-text(250px);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { ConfirmService } from '../../../core'
|
||||
import { ConfirmService, ServerService } from '../../../core'
|
||||
import { RestPagination, RestTable, UserService } from '../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { User } from '../../../../../../shared'
|
||||
import { UserBanModalComponent } from '@app/shared/moderation'
|
||||
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-list',
|
||||
|
@ -12,38 +14,139 @@ import { User } from '../../../../../../shared'
|
|||
styleUrls: [ './user-list.component.scss' ]
|
||||
})
|
||||
export class UserListComponent extends RestTable implements OnInit {
|
||||
@ViewChild('userBanModal') userBanModal: UserBanModalComponent
|
||||
|
||||
users: User[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: 1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
selectedUsers: User[] = []
|
||||
bulkUserActions: DropdownAction<User[]>[] = []
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private serverService: ServerService,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
get requiresEmailVerification () {
|
||||
return this.serverService.getConfig().signup.requiresEmailVerification
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
this.initialize()
|
||||
|
||||
this.bulkUserActions = [
|
||||
{
|
||||
label: this.i18n('Delete'),
|
||||
handler: users => this.removeUsers(users)
|
||||
},
|
||||
{
|
||||
label: this.i18n('Ban'),
|
||||
handler: users => this.openBanUserModal(users),
|
||||
isDisplayed: users => users.every(u => u.blocked === false)
|
||||
},
|
||||
{
|
||||
label: this.i18n('Unban'),
|
||||
handler: users => this.unbanUsers(users),
|
||||
isDisplayed: users => users.every(u => u.blocked === true)
|
||||
},
|
||||
{
|
||||
label: this.i18n('Set Email as Verified'),
|
||||
handler: users => this.setEmailsAsVerified(users),
|
||||
isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
openBanUserModal (users: User[]) {
|
||||
for (const user of users) {
|
||||
if (user.username === 'root') {
|
||||
this.notifier.error(this.i18n('You cannot ban root.'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.userBanModal.openModal(users)
|
||||
}
|
||||
|
||||
onUserChanged () {
|
||||
this.loadData()
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.userService.getUsers(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.users = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
async unbanUsers (users: User[]) {
|
||||
const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
const res = await this.confirmService.confirm(message, this.i18n('Unban'))
|
||||
if (res === false) return
|
||||
|
||||
this.userService.unbanUsers(users)
|
||||
.subscribe(
|
||||
() => {
|
||||
const message = this.i18n('{{num}} users unbanned.', { num: users.length })
|
||||
|
||||
this.notifier.success(message)
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
async removeUsers (users: User[]) {
|
||||
for (const user of users) {
|
||||
if (user.username === 'root') {
|
||||
this.notifier.error(this.i18n('You cannot delete root.'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const message = this.i18n('If you remove these users, you will not be able to create others with the same username!')
|
||||
const res = await this.confirmService.confirm(message, this.i18n('Delete'))
|
||||
if (res === false) return
|
||||
|
||||
this.userService.removeUser(users).subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length }))
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
async setEmailsAsVerified (users: User[]) {
|
||||
this.userService.updateUsers(users, { emailVerified: true }).subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length }))
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
isInSelectionMode () {
|
||||
return this.selectedUsers.length !== 0
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
this.selectedUsers = []
|
||||
|
||||
this.userService.getUsers(this.pagination, this.sort, this.search)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.users = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<div class="admin-sub-header">
|
||||
<div i18n class="form-sub-title">Muted accounts</div>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
[value]="blockedAccounts" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n>Account</th>
|
||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-accountBlock>
|
||||
<tr>
|
||||
<td>{{ accountBlock.blockedAccount.nameWithHost }}</td>
|
||||
<td>{{ accountBlock.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<button class="unblock-button" (click)="unblockAccount(accountBlock)" i18n>Unmute</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -0,0 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.unblock-button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { RestPagination, RestTable } from '@app/shared'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-blocklist',
|
||||
styleUrls: [ './my-account-blocklist.component.scss' ],
|
||||
templateUrl: './my-account-blocklist.component.html'
|
||||
})
|
||||
export class MyAccountBlocklistComponent extends RestTable implements OnInit {
|
||||
blockedAccounts: AccountBlock[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private blocklistService: BlocklistService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
unblockAccount (accountBlock: AccountBlock) {
|
||||
const blockedAccount = accountBlock.blockedAccount
|
||||
|
||||
this.blocklistService.unblockAccountByUser(blockedAccount)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
|
||||
|
||||
this.loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.blocklistService.getUserAccountBlocklist(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.blockedAccounts = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<div class="admin-sub-header">
|
||||
<div i18n class="form-sub-title">Muted instances</div>
|
||||
</div>
|
||||
|
||||
<p-table
|
||||
[value]="blockedServers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage"
|
||||
[sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)"
|
||||
>
|
||||
|
||||
<ng-template pTemplate="header">
|
||||
<tr>
|
||||
<th i18n>Instance</th>
|
||||
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
||||
<ng-template pTemplate="body" let-serverBlock>
|
||||
<tr>
|
||||
<td>{{ serverBlock.blockedServer.host }}</td>
|
||||
<td>{{ serverBlock.createdAt }}</td>
|
||||
<td class="action-cell">
|
||||
<button class="unblock-button" (click)="unblockServer(serverBlock)" i18n>Unmute</button>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</p-table>
|
|
@ -0,0 +1,7 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.unblock-button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Notifier } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { RestPagination, RestTable } from '@app/shared'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { ServerBlock } from '../../../../../shared'
|
||||
import { BlocklistService } from '@app/shared/blocklist'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-server-blocklist',
|
||||
styleUrls: [ './my-account-server-blocklist.component.scss' ],
|
||||
templateUrl: './my-account-server-blocklist.component.html'
|
||||
})
|
||||
export class MyAccountServerBlocklistComponent extends RestTable implements OnInit {
|
||||
blockedServers: ServerBlock[] = []
|
||||
totalRecords = 0
|
||||
rowsPerPage = 10
|
||||
sort: SortMeta = { field: 'createdAt', order: -1 }
|
||||
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
|
||||
|
||||
constructor (
|
||||
private notifier: Notifier,
|
||||
private blocklistService: BlocklistService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
unblockServer (serverBlock: ServerBlock) {
|
||||
const host = serverBlock.blockedServer.host
|
||||
|
||||
this.blocklistService.unblockServerByUser(host)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
|
||||
|
||||
this.loadData()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.blocklistService.getUserServerBlocklist(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.blockedServers = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<div class="top-buttons">
|
||||
<div class="history-switch">
|
||||
<p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch>
|
||||
<label i18n>History enabled</label>
|
||||
</div>
|
||||
|
||||
<div class="delete-history">
|
||||
<button (click)="deleteHistory()" i18n>Delete history</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
|
||||
|
||||
<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
|
||||
<div *ngFor="let videos of videoPages;" class="videos-page">
|
||||
<div class="video" *ngFor="let video of videos">
|
||||
<my-video-thumbnail [video]="video"></my-video-thumbnail>
|
||||
|
||||
<div class="video-info">
|
||||
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
|
||||
<span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
|
||||
<a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,99 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.no-history {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
font-weight: $font-semibold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.top-buttons {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
|
||||
.history-switch {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
label {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.delete-history {
|
||||
font-size: 15px;
|
||||
|
||||
button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
@include row-blocks;
|
||||
|
||||
my-video-thumbnail {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.video-info {
|
||||
flex-grow: 1;
|
||||
|
||||
.video-info-name {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
color: var(--mainForegroundColor);
|
||||
display: block;
|
||||
width: fit-content;
|
||||
font-size: 18px;
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
|
||||
.video-info-date-views {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.video-info-account {
|
||||
@include disable-default-a-behaviour;
|
||||
|
||||
display: block;
|
||||
width: fit-content;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: $grey-foreground-color;
|
||||
|
||||
&:hover {
|
||||
color: $grey-foreground-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: $small-view) {
|
||||
.video {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
text-align: center;
|
||||
|
||||
.video-info-name {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input[type=checkbox] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
my-video-thumbnail {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.video-buttons {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { Location } from '@angular/common'
|
||||
import { immutableAssign } from '@app/shared/misc/utils'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
import { AuthService } from '../../core/auth'
|
||||
import { ConfirmService } from '../../core/confirm'
|
||||
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
|
||||
import { VideoService } from '../../shared/video/video.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
import { UserHistoryService } from '@app/shared/users/user-history.service'
|
||||
import { UserService } from '@app/shared'
|
||||
import { Notifier } from '@app/core'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-history',
|
||||
templateUrl: './my-account-history.component.html',
|
||||
styleUrls: [ './my-account-history.component.scss' ]
|
||||
})
|
||||
export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
|
||||
titlePage: string
|
||||
currentRoute = '/my-account/history/videos'
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 5,
|
||||
totalItems: null
|
||||
}
|
||||
videosHistoryEnabled: boolean
|
||||
|
||||
protected baseVideoWidth = -1
|
||||
protected baseVideoHeight = 155
|
||||
|
||||
constructor (
|
||||
protected router: Router,
|
||||
protected route: ActivatedRoute,
|
||||
protected authService: AuthService,
|
||||
protected userService: UserService,
|
||||
protected notifier: Notifier,
|
||||
protected location: Location,
|
||||
protected screenService: ScreenService,
|
||||
protected i18n: I18n,
|
||||
private confirmService: ConfirmService,
|
||||
private videoService: VideoService,
|
||||
private userHistoryService: UserHistoryService
|
||||
) {
|
||||
super()
|
||||
|
||||
this.titlePage = this.i18n('My videos history')
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
super.ngOnInit()
|
||||
|
||||
this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
getVideosObservable (page: number) {
|
||||
const newPagination = immutableAssign(this.pagination, { currentPage: page })
|
||||
|
||||
return this.userHistoryService.getUserVideosHistory(newPagination)
|
||||
}
|
||||
|
||||
generateSyndicationList () {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
onVideosHistoryChange () {
|
||||
this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled })
|
||||
.subscribe(
|
||||
() => {
|
||||
const message = this.videosHistoryEnabled === true ?
|
||||
this.i18n('Videos history is enabled') :
|
||||
this.i18n('Videos history is disabled')
|
||||
|
||||
this.notifier.success(message)
|
||||
|
||||
this.authService.refreshUserInformation()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
async deleteHistory () {
|
||||
const title = this.i18n('Delete videos history')
|
||||
const message = this.i18n('Are you sure you want to delete all your videos history?')
|
||||
|
||||
const res = await this.confirmService.confirm(message, title)
|
||||
if (res !== true) return
|
||||
|
||||
this.userHistoryService.deleteUserVideosHistory()
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Videos history deleted'))
|
||||
|
||||
this.reloadVideos()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<div class="header">
|
||||
<a routerLink="/my-account/settings" fragment="notifications" i18n>
|
||||
<my-global-icon iconName="cog"></my-global-icon>
|
||||
Notification preferences
|
||||
</a>
|
||||
|
||||
<button (click)="markAllAsRead()" i18n>
|
||||
<my-global-icon iconName="circle-tick"></my-global-icon>
|
||||
Mark all as read
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<my-user-notifications #userNotification></my-user-notifications>
|
|
@ -0,0 +1,25 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
a {
|
||||
@include peertube-button-link;
|
||||
@include grey-button;
|
||||
@include button-with-icon(18px, 3px, -1px);
|
||||
}
|
||||
|
||||
button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
@include button-with-icon(20px, 3px, -1px);
|
||||
}
|
||||
}
|
||||
|
||||
my-user-notifications {
|
||||
font-size: 15px;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { Component, ViewChild } from '@angular/core'
|
||||
import { UserNotificationsComponent } from '@app/shared'
|
||||
|
||||
@Component({
|
||||
templateUrl: './my-account-notifications.component.html',
|
||||
styleUrls: [ './my-account-notifications.component.scss' ]
|
||||
})
|
||||
export class MyAccountNotificationsComponent {
|
||||
@ViewChild('userNotification') userNotification: UserNotificationsComponent
|
||||
|
||||
markAllAsRead () {
|
||||
this.userNotification.markAllAsRead()
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Accept ownership</h4>
|
||||
<span class="close" aria-label="Close" role="button" (click)="dismiss()"></span>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" [formGroup]="form">
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { FormReactive } from '@app/shared'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { VideoOwnershipService } from '@app/shared/video-ownership'
|
||||
|
@ -8,7 +8,6 @@ import { VideoAcceptOwnershipValidatorsService } from '@app/shared/forms/form-va
|
|||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { AuthService } from '@app/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
@Component({
|
||||
|
@ -31,7 +30,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
|
|||
protected formValidatorService: FormValidatorService,
|
||||
private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService,
|
||||
private videoOwnershipService: VideoOwnershipService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private authService: AuthService,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private modalService: NgbModal,
|
||||
|
@ -68,12 +67,12 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
|
|||
.acceptOwnership(videoChangeOwnership.id, { channelId: channel })
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership accepted'))
|
||||
this.notifier.success(this.i18n('Ownership accepted'))
|
||||
if (this.accepted) this.accepted.emit()
|
||||
this.videoChangeOwnership = undefined
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
<td class="action-cell">
|
||||
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
||||
<my-button i18n label="Accept"
|
||||
icon="icon-tick"
|
||||
icon="tick"
|
||||
(click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
||||
<my-button i18n label="Refuse"
|
||||
icon="icon-cross"
|
||||
icon="cross"
|
||||
(click)="refuse(videoChangeOwnership)">Refuse</my-button>
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Notifier } from '@app/core'
|
||||
import { RestPagination, RestTable } from '@app/shared'
|
||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||
import { VideoChangeOwnership } from '../../../../../shared'
|
||||
import { VideoOwnershipService } from '@app/shared/video-ownership'
|
||||
import { Account } from '@app/shared/account/account.model'
|
||||
import { MyAccountAcceptOwnershipComponent }
|
||||
from '@app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component'
|
||||
import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-ownership',
|
||||
|
@ -23,27 +21,14 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
|
|||
@ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent
|
||||
|
||||
constructor (
|
||||
private notificationsService: NotificationsService,
|
||||
private videoOwnershipService: VideoOwnershipService,
|
||||
private i18n: I18n
|
||||
private notifier: Notifier,
|
||||
private videoOwnershipService: VideoOwnershipService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.loadSort()
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.videoChangeOwnerships = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
)
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
createByString (account: Account) {
|
||||
|
@ -62,7 +47,19 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
|
|||
this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id)
|
||||
.subscribe(
|
||||
() => this.loadData(),
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.videoOwnershipService.getOwnershipChanges(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
resultList => {
|
||||
this.videoChangeOwnerships = resultList.data
|
||||
this.totalRecords = resultList.total
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
|
|||
import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
|
||||
import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
|
||||
import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
|
||||
import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
|
||||
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
|
||||
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
|
||||
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
|
||||
|
||||
const myAccountRoutes: Routes = [
|
||||
{
|
||||
|
@ -94,6 +98,42 @@ const myAccountRoutes: Routes = [
|
|||
title: 'Ownership changes'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blocklist/accounts',
|
||||
component: MyAccountBlocklistComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Muted accounts'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'blocklist/servers',
|
||||
component: MyAccountServerBlocklistComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Muted instances'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'history/videos',
|
||||
component: MyAccountHistoryComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Videos history'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'notifications',
|
||||
component: MyAccountNotificationsComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: 'Notifications'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { FormReactive, UserService } from '../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
|
||||
import { filter } from 'rxjs/operators'
|
||||
import { AuthService } from '@app/core'
|
||||
import { User } from '../../../../../../shared'
|
||||
|
||||
@Component({
|
||||
|
@ -20,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
|
|||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private authService: AuthService,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
|
@ -50,7 +49,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
|
|||
|
||||
this.userService.changePassword(currentPassword, newPassword).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Password updated.'))
|
||||
this.notifier.success(this.i18n('Password updated.'))
|
||||
|
||||
this.form.reset()
|
||||
this.error = null
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { AuthService, ConfirmService, RedirectService } from '../../../core'
|
||||
import { UserService } from '../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
|
@ -15,7 +15,7 @@ export class MyAccountDangerZoneComponent {
|
|||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private confirmService: ConfirmService,
|
||||
private redirectService: RedirectService,
|
||||
|
@ -34,13 +34,13 @@ export class MyAccountDangerZoneComponent {
|
|||
|
||||
this.userService.deleteMe().subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.'))
|
||||
this.notifier.success(this.i18n('Your account is deleted.'))
|
||||
|
||||
this.authService.logout()
|
||||
this.redirectService.redirectToHomepage()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export * from './my-account-notification-preferences.component'
|
|
@ -0,0 +1,19 @@
|
|||
<div class="custom-row">
|
||||
<div i18n>Activities</div>
|
||||
<div i18n>Web</div>
|
||||
<div i18n *ngIf="emailEnabled">Email</div>
|
||||
</div>
|
||||
|
||||
<div class="custom-row" *ngFor="let notificationType of notificationSettingKeys">
|
||||
<ng-container *ngIf="hasUserRight(notificationType)">
|
||||
<div>{{ labelNotifications[notificationType] }}</div>
|
||||
|
||||
<div>
|
||||
<p-inputSwitch [(ngModel)]="webNotifications[notificationType]" (onChange)="updateWebSetting(notificationType, $event.checked)"></p-inputSwitch>
|
||||
</div>
|
||||
|
||||
<div *ngIf="emailEnabled">
|
||||
<p-inputSwitch [(ngModel)]="emailNotifications[notificationType]" (onChange)="updateEmailSetting(notificationType, $event.checked)"></p-inputSwitch>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
|
@ -0,0 +1,25 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.custom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.10);
|
||||
|
||||
&:first-child {
|
||||
font-size: 16px;
|
||||
|
||||
& > div {
|
||||
font-weight: $font-semibold;
|
||||
}
|
||||
}
|
||||
|
||||
& > div {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
padding: 10px
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { User } from '@app/shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subject } from 'rxjs'
|
||||
import { UserNotificationSetting, UserNotificationSettingValue, UserRight } from '../../../../../../shared'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-notification-preferences',
|
||||
templateUrl: './my-account-notification-preferences.component.html',
|
||||
styleUrls: [ './my-account-notification-preferences.component.scss' ]
|
||||
})
|
||||
export class MyAccountNotificationPreferencesComponent implements OnInit {
|
||||
@Input() user: User = null
|
||||
@Input() userInformationLoaded: Subject<any>
|
||||
|
||||
notificationSettingKeys: (keyof UserNotificationSetting)[] = []
|
||||
emailNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
|
||||
webNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
|
||||
labelNotifications: { [ id in keyof UserNotificationSetting ]: string } = {} as any
|
||||
rightNotifications: { [ id in keyof Partial<UserNotificationSetting> ]: UserRight } = {} as any
|
||||
emailEnabled: boolean
|
||||
|
||||
private savePreferences = debounce(this.savePreferencesImpl.bind(this), 500)
|
||||
|
||||
constructor (
|
||||
private i18n: I18n,
|
||||
private userNotificationService: UserNotificationService,
|
||||
private serverService: ServerService,
|
||||
private notifier: Notifier
|
||||
) {
|
||||
this.labelNotifications = {
|
||||
newVideoFromSubscription: this.i18n('New video from your subscriptions'),
|
||||
newCommentOnMyVideo: this.i18n('New comment on your video'),
|
||||
videoAbuseAsModerator: this.i18n('New video abuse on local video'),
|
||||
blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
|
||||
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
|
||||
myVideoImportFinished: this.i18n('Video import finished'),
|
||||
newUserRegistration: this.i18n('A new user registered on your instance'),
|
||||
newFollow: this.i18n('You or your channel(s) has a new follower'),
|
||||
commentMention: this.i18n('Someone mentioned you in video comments')
|
||||
}
|
||||
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
|
||||
|
||||
this.rightNotifications = {
|
||||
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
|
||||
newUserRegistration: UserRight.MANAGE_USERS
|
||||
}
|
||||
|
||||
this.emailEnabled = this.serverService.getConfig().email.enabled
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.userInformationLoaded.subscribe(() => this.loadNotificationSettings())
|
||||
}
|
||||
|
||||
hasUserRight (field: keyof UserNotificationSetting) {
|
||||
const rightToHave = this.rightNotifications[field]
|
||||
if (!rightToHave) return true // No rights needed
|
||||
|
||||
return this.user.hasRight(rightToHave)
|
||||
}
|
||||
|
||||
updateEmailSetting (field: keyof UserNotificationSetting, value: boolean) {
|
||||
if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.EMAIL
|
||||
else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.EMAIL
|
||||
|
||||
this.savePreferences()
|
||||
}
|
||||
|
||||
updateWebSetting (field: keyof UserNotificationSetting, value: boolean) {
|
||||
if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.WEB
|
||||
else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.WEB
|
||||
|
||||
this.savePreferences()
|
||||
}
|
||||
|
||||
private savePreferencesImpl () {
|
||||
this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notifier.success(this.i18n('Preferences saved'), undefined, 2000)
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
private loadNotificationSettings () {
|
||||
for (const key of Object.keys(this.user.notificationSettings)) {
|
||||
const value = this.user.notificationSettings[key]
|
||||
this.emailNotifications[key] = value & UserNotificationSettingValue.EMAIL
|
||||
|
||||
this.webNotifications[key] = value & UserNotificationSettingValue.WEB
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { FormReactive, UserService } from '../../../shared'
|
||||
import { User } from '@app/shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
|
@ -21,7 +21,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
|
|||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -53,7 +53,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
|
|||
this.user.account.displayName = displayName
|
||||
this.user.account.description = description
|
||||
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Profile updated.'))
|
||||
this.notifier.success(this.i18n('Profile updated.'))
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
<span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
|
||||
</div>
|
||||
|
||||
<ng-template [ngIf]="user && user.account">
|
||||
<div i18n class="account-title">Profile</div>
|
||||
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
|
||||
</ng-template>
|
||||
<div i18n class="account-title">Profile</div>
|
||||
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
|
||||
|
||||
<div i18n class="account-title" id="notifications">Notifications</div>
|
||||
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
|
||||
|
||||
<div i18n class="account-title">Password</div>
|
||||
<my-account-change-password></my-account-change-password>
|
||||
|
@ -16,4 +17,4 @@
|
|||
<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
|
||||
|
||||
<div i18n class="account-title">Danger zone</div>
|
||||
<my-account-danger-zone [user]="user"></my-account-danger-zone>
|
||||
<my-account-danger-zone [user]="user"></my-account-danger-zone>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { BytesPipe } from 'ngx-pipes'
|
||||
import { AuthService } from '../../core'
|
||||
import { User } from '../../shared'
|
||||
|
@ -19,7 +19,7 @@ export class MyAccountSettingsComponent implements OnInit {
|
|||
constructor (
|
||||
private userService: UserService,
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private i18n: I18n
|
||||
) {}
|
||||
|
||||
|
@ -48,12 +48,12 @@ export class MyAccountSettingsComponent implements OnInit {
|
|||
this.userService.changeAvatar(formData)
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
|
||||
this.notifier.success(this.i18n('Avatar changed.'))
|
||||
|
||||
this.user.updateAccountAvatar(data.avatar)
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,19 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox
|
||||
inputName="autoPlayVideo" formControlName="autoPlayVideo"
|
||||
i18n-labelText labelText="Automatically plays video"
|
||||
></my-peertube-checkbox>
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="webTorrentEnabled" formControlName="webTorrentEnabled"
|
||||
i18n-labelText labelText="Use WebTorrent to exchange parts of the video with others"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="autoPlayVideo" formControlName="autoPlayVideo"
|
||||
i18n-labelText labelText="Automatically plays video"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<input type="submit" i18n-value value="Save" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { UserUpdateMe } from '../../../../../../shared'
|
||||
import { AuthService } from '../../../core'
|
||||
import { FormReactive, User, UserService } from '../../../shared'
|
||||
|
@ -19,7 +19,7 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
|||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
|
@ -29,12 +29,14 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
|||
ngOnInit () {
|
||||
this.buildForm({
|
||||
nsfwPolicy: null,
|
||||
webTorrentEnabled: null,
|
||||
autoPlayVideo: null
|
||||
})
|
||||
|
||||
this.userInformationLoaded.subscribe(() => {
|
||||
this.form.patchValue({
|
||||
nsfwPolicy: this.user.nsfwPolicy,
|
||||
webTorrentEnabled: this.user.webTorrentEnabled,
|
||||
autoPlayVideo: this.user.autoPlayVideo === true
|
||||
})
|
||||
})
|
||||
|
@ -42,20 +44,22 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
|
|||
|
||||
updateDetails () {
|
||||
const nsfwPolicy = this.form.value['nsfwPolicy']
|
||||
const webTorrentEnabled = this.form.value['webTorrentEnabled']
|
||||
const autoPlayVideo = this.form.value['autoPlayVideo']
|
||||
const details: UserUpdateMe = {
|
||||
nsfwPolicy,
|
||||
webTorrentEnabled,
|
||||
autoPlayVideo
|
||||
}
|
||||
|
||||
this.userService.updateMyProfile(details).subscribe(
|
||||
() => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Information updated.'))
|
||||
this.notifier.success(this.i18n('Information updated.'))
|
||||
|
||||
this.authService.refreshUserInformation()
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { UserSubscriptionService } from '@app/shared/user-subscription'
|
||||
|
@ -21,7 +21,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
|||
|
||||
constructor (
|
||||
private userSubscriptionService: UserSubscriptionService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private i18n: I18n
|
||||
) {}
|
||||
|
||||
|
@ -37,7 +37,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
|
|||
this.pagination.totalItems = res.total
|
||||
},
|
||||
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
error => this.notifier.error(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService, Notifier } from '@app/core'
|
||||
import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
|
||||
import { VideoChannelCreate } from '../../../../../shared/models/videos'
|
||||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||
import { AuthService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
|
||||
|
@ -21,7 +20,7 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
|
|||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private videoChannelValidatorsService: VideoChannelValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private router: Router,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private i18n: I18n
|
||||
|
@ -56,8 +55,8 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
|
|||
this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe(
|
||||
() => {
|
||||
this.authService.refreshUserInformation()
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
|
||||
this.notifier.success(
|
||||
this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName })
|
||||
)
|
||||
this.router.navigate([ '/my-account', 'video-channels' ])
|
||||
|
|
|
@ -4,7 +4,11 @@ import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
|||
export abstract class MyAccountVideoChannelEdit extends FormReactive {
|
||||
// We need it even in the create component because it's used in the edit template
|
||||
videoChannelToUpdate: VideoChannel
|
||||
instanceHost: string
|
||||
|
||||
abstract isCreation (): boolean
|
||||
abstract getFormButtonTitle (): string
|
||||
|
||||
// FIXME: We need this method so angular does not complain in the child template
|
||||
onAvatarChange (formData: FormData) { /* empty */ }
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||
import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
|
||||
import { VideoChannelUpdate } from '../../../../../shared/models/videos'
|
||||
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { AuthService, ServerService } from '@app/core'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
|
||||
|
@ -17,18 +16,16 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators
|
|||
styleUrls: [ './my-account-video-channel-edit.component.scss' ]
|
||||
})
|
||||
export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
|
||||
@ViewChild('avatarfileInput') avatarfileInput
|
||||
|
||||
error: string
|
||||
|
||||
videoChannelToUpdate: VideoChannel
|
||||
|
||||
private paramsSub: Subscription
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private authService: AuthService,
|
||||
private videoChannelValidatorsService: VideoChannelValidatorsService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute,
|
||||
private videoChannelService: VideoChannelService,
|
||||
|
@ -81,10 +78,11 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
|
|||
this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
|
||||
() => {
|
||||
this.authService.refreshUserInformation()
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
|
||||
this.notifier.success(
|
||||
this.i18n('Video channel {{videoChannelName}} updated.', { videoChannelName: videoChannelUpdate.displayName })
|
||||
)
|
||||
|
||||
this.router.navigate([ '/my-account', 'video-channels' ])
|
||||
},
|
||||
|
||||
|
@ -96,12 +94,12 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
|
|||
this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
|
||||
.subscribe(
|
||||
data => {
|
||||
this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
|
||||
this.notifier.success(this.i18n('Avatar changed.'))
|
||||
|
||||
this.videoChannelToUpdate.updateAvatar(data.avatar)
|
||||
},
|
||||
|
||||
err => this.notificationsService.error(this.i18n('Error'), err.message)
|
||||
err => this.notifier.error(err.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="video-channels-header">
|
||||
<a class="create-button" routerLink="create">
|
||||
<span class="icon icon-add"></span>
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create another video channel</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import '_mixins';
|
||||
|
||||
.create-button {
|
||||
@include create-button('../../../assets/images/global/add.svg');
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
/deep/ .action-button {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Notifier } from '@app/core'
|
||||
import { AuthService } from '../../core/auth'
|
||||
import { ConfirmService } from '../../core/confirm'
|
||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
|
@ -20,7 +20,7 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
|||
|
||||
constructor (
|
||||
private authService: AuthService,
|
||||
private notificationsService: NotificationsService,
|
||||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private videoChannelService: VideoChannelService,
|
||||
private i18n: I18n
|
||||
|
@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
|||
async deleteVideoChannel (videoChannel: VideoChannel) {
|
||||
const res = await this.confirmService.confirmWithInput(
|
||||
this.i18n(
|
||||
'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.',
|
||||
{ videoChannelName: videoChannel.displayName }
|
||||
'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' +
|
||||
'and you will not be able to create another channel with the same name ({{channelName}})!',
|
||||
{ channelDisplayName: videoChannel.displayName, channelName: videoChannel.name }
|
||||
),
|
||||
this.i18n(
|
||||
'Please type the display name of the video channel ({{displayName}}) to confirm',
|
||||
{ displayName: videoChannel.displayName }
|
||||
),
|
||||
this.i18n('Please type the name of the video channel to confirm'),
|
||||
videoChannel.displayName,
|
||||
this.i18n('Delete')
|
||||
)
|
||||
|
@ -46,15 +50,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
|||
|
||||
this.videoChannelService.removeVideoChannel(videoChannel)
|
||||
.subscribe(
|
||||
status => {
|
||||
() => {
|
||||
this.loadVideoChannels()
|
||||
this.notificationsService.success(
|
||||
this.i18n('Success'),
|
||||
this.notifier.success(
|
||||
this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName })
|
||||
)
|
||||
},
|
||||
|
||||
error => this.notificationsService.error(this.i18n('Error'), error.message)
|
||||
error => this.notifier.error(error.message)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue