Merge branch 'develop' into pr/1217

This commit is contained in:
Chocobozzz 2019-02-11 11:52:34 +01:00
commit 88108880bb
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
888 changed files with 54348 additions and 33154 deletions

View File

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

1
.gitignore vendored
View File

@ -9,6 +9,7 @@
/test4/
/test5/
/test6/
/server/tests/fixtures/video_high_bitrate_1080p.mp4
# Production
/storage/

View File

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

View File

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

View File

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

View File

@ -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
View File

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

126
README.md

File diff suppressed because one or more lines are too long

View File

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

View File

@ -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) {

View File

@ -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",

View File

@ -1,24 +1,28 @@
<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 *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
</div>
<div class="short-description">
<div>{{ shortDescription }}</div>
</div>
</div>
<div class="description">
<div class="description">
<div i18n class="section-title">Description</div>
<div [innerHTML]="descriptionHTML"></div>
</div>
</div>
<div class="terms" id="terms-section">
<div class="terms" id="terms-section">
<div i18n class="section-title">Terms</div>
<div [innerHTML]="termsHTML"></div>
</div>
</div>
<div class="signup">
<div class="signup">
<div i18n class="section-title">Signup</div>
<div *ngIf="isSignupAllowed">
@ -36,4 +40,13 @@
<div i18n *ngIf="isSignupAllowed === false">
User registration is currently not allowed.
</div>
</div>
</div>
<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>
<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>

View File

@ -2,9 +2,19 @@
@import '_mixins';
.about-instance-title {
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 {

View File

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

View File

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

View File

@ -0,0 +1,11 @@
@import 'variables';
@import 'mixins';
input[type=text] {
@include peertube-input-text(340px);
display: block;
}
textarea {
@include peertube-textarea(100%, 200px);
}

View File

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

View File

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

View File

@ -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: [

View File

@ -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',

View File

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

View File

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

View File

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

View File

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

View File

@ -7,63 +7,54 @@
<div i18n class="inner-form-title">Instance</div>
<ng-container formGroupName="instance">
<div class="form-group">
<label i18n for="instanceName">Name</label>
<input
type="text" id="instanceName"
formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
>
<div *ngIf="formErrors.instanceName" class="form-error">
{{ formErrors.instanceName }}
</div>
<div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
</div>
<div class="form-group">
<label i18n for="instanceShortDescription">Short description</label>
<textarea
id="instanceShortDescription" formControlName="instanceShortDescription"
[ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
id="instanceShortDescription" formControlName="shortDescription"
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
></textarea>
<div *ngIf="formErrors.instanceShortDescription" class="form-error">
{{ formErrors.instanceShortDescription }}
</div>
<div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</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'] }"
id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
[classes]="{ 'input-error': formErrors['instance.description'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instanceDescription" class="form-error">
{{ formErrors.instanceDescription }}
</div>
<div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</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'] }"
id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
[ngClass]="{ 'input-error': formErrors['instance.terms'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instanceTerms" class="form-error">
{{ formErrors.instanceTerms }}
</div>
<div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
</div>
<div class="form-group">
<label i18n for="instanceDefaultClientRoute">Default client route</label>
<div class="peertube-select-container">
<select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
<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.instanceDefaultClientRoute" class="form-error">
{{ formErrors.instanceDefaultClientRoute }}
</div>
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
</div>
<div class="form-group">
@ -74,94 +65,111 @@
></my-help>
<div class="peertube-select-container">
<select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
<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.instanceDefaultNSFWPolicy" class="form-error">
{{ formErrors.instanceDefaultNSFWPolicy }}
</div>
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Signup</div>
<ng-container formGroupName="signup">
<div class="form-group">
<my-peertube-checkbox
inputName="signupEnabled" formControlName="signupEnabled"
inputName="signupEnabled" formControlName="enabled"
i18n-labelText labelText="Signup enabled"
></my-peertube-checkbox>
</div>
<div class="form-group">
<my-peertube-checkbox *ngIf="isSignupEnabled()"
inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
i18n-labelText labelText="Signup requires email verification"
></my-peertube-checkbox>
</div>
<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'] }"
formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
>
<div *ngIf="formErrors.signupLimit" class="form-error">
{{ formErrors.signupLimit }}
</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>
<div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Users</div>
<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="userVideoQuota">
<select id="userVideoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.userVideoQuota" class="form-error">
{{ formErrors.userVideoQuota }}
</div>
<div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</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">
<select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
{{ videoQuotaDailyOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
{{ formErrors.userVideoQuotaDaily }}
<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" formGroupName="contactForm">
<my-peertube-checkbox
inputName="enableContactForm" formControlName="enabled"
i18n-labelText labelText="Enable contact form"
></my-peertube-checkbox>
</div>
</ng-template>
</ngb-tab>
@ -169,6 +177,9 @@
<ng-template ngbTabContent>
<div i18n class="inner-form-title">Twitter</div>
<ng-container formGroupName="services">
<ng-container formGroupName="twitter">
<div class="form-group">
<label i18n for="signupLimit">Your Twitter username</label>
<my-help
@ -177,20 +188,24 @@
></my-help>
<input
type="text" id="servicesTwitterUsername"
formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
>
<div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
{{ formErrors.servicesTwitterUsername }}
</div>
<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="servicesTwitterWhitelisted"
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>
</ng-template>
</ngb-tab>
@ -199,36 +214,48 @@
<div i18n class="inner-form-title">Transcoding</div>
<ng-container formGroupName="transcoding">
<div class="form-group">
<my-peertube-checkbox
inputName="transcodingEnabled" formControlName="transcodingEnabled"
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 [ngIf]="isTranscodingEnabled()">
<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="transcodingThreads">
<select id="transcodingThreads" formControlName="threads">
<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 *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]="getResolutionKey(resolution)"
[inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
i18n-labelText labelText="Resolution {{resolution}} enabled"
></my-peertube-checkbox>
</div>
</ng-template>
</ng-container>
</ng-container>
</ng-container>
<div i18n class="inner-form-title">
Cache
@ -239,30 +266,30 @@
></my-help>
</div>
<div class="form-group">
<ng-container formGroupName="cache">
<div class="form-group" formGroupName="previews">
<label i18n for="cachePreviewsSize">Previews cache size</label>
<input
type="text" id="cachePreviewsSize"
formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
>
<div *ngIf="formErrors.cachePreviewsSize" class="form-error">
{{ formErrors.cachePreviewsSize }}
</div>
<div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
</div>
<div class="form-group">
<label i18n for="cachePreviewsSize">Video captions cache size</label>
<div class="form-group" formGroupName="captions">
<label i18n for="cacheCaptionsSize">Video captions cache size</label>
<input
type="text" id="cacheCaptionsSize"
formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }"
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
>
<div *ngIf="formErrors.cacheCaptionsSize" class="form-error">
{{ formErrors.cacheCaptionsSize }}
</div>
<div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Customizations</div>
<ng-container formGroupName="instance">
<ng-container formGroupName="customizations">
<div class="form-group">
<label i18n for="customizationJavascript">JavaScript</label>
<my-help
@ -270,12 +297,10 @@
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'] }"
id="customizationJavascript" formControlName="javascript"
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
></textarea>
<div *ngIf="formErrors.customizationJavascript" class="form-error">
{{ formErrors.customizationJavascript }}
</div>
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
</div>
<div class="form-group">
@ -300,13 +325,14 @@
"
></my-help>
<textarea
id="customizationCSS" formControlName="customizationCSS"
[ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
id="customizationCSS" formControlName="css"
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
></textarea>
<div *ngIf="formErrors.customizationCSS" class="form-error">
{{ formErrors.customizationCSS }}
</div>
<div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
</div>
</ng-container>
</ng-container>
</ng-template>
</ngb-tab>
</ngb-tabset>

View File

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

View File

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

View File

@ -0,0 +1,10 @@
@import '_variables';
@import '_mixins';
.caption {
justify-content: flex-end;
input {
@include peertube-input-text(250px);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)),

View File

@ -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,7 +13,7 @@ export class RedundancyCheckboxComponent {
@Input() host: string
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private redundancyService: RedundancyService,
private i18n: I18n
) { }
@ -24,13 +24,10 @@ export class RedundancyCheckboxComponent {
() => {
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)
)
}
}

View File

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

View File

@ -0,0 +1,2 @@
export * from './instance-account-blocklist.component'
export * from './instance-server-blocklist.component'

View File

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

View File

@ -0,0 +1,7 @@
@import '_variables';
@import '_mixins';
.unblock-button {
@include peertube-button;
@include grey-button;
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
@import '_variables';
@import '_mixins';
.unblock-button {
@include peertube-button;
@include grey-button;
}

View File

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

View File

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

View File

@ -10,6 +10,7 @@
font-weight: $font-semibold;
min-width: 200px;
display: inline-block;
vertical-align: top;
}
.moderation-expanded-text {

View File

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

View File

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

View File

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

View File

@ -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,28 +45,25 @@ 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.')
)
this.notifier.success(this.i18n('Comment updated.'))
this.commentUpdated.emit(moderationComment)
this.hideModerationCommentModal()
this.hide()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}

View File

@ -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,11 +51,11 @@
<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>

View File

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

View File

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

View File

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

View File

@ -1,2 +1,3 @@
export * from './user-create.component'
export * from './user-update.component'
export * from './user-password.component'

View File

@ -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' ])
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>
<a i18n-title title="Go to the account page" target="_blank" rel="noopener noreferrer" [routerLink]="[ '/accounts/' + user.username ]">
{{ user.username }}
<span *ngIf="user.blocked" class="banned-info">(banned)</span>
<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">
&#x2713; {{ 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>

View File

@ -2,7 +2,7 @@
@import '_mixins';
.add-button {
@include create-button('../../../../assets/images/global/add.svg');
@include create-button;
}
tr.banned {
@ -16,3 +16,11 @@ tr.banned {
.ban-reason-label {
font-weight: $font-semibold;
}
.caption {
justify-content: space-between;
input {
@include peertube-input-text(250px);
}
}

View File

@ -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()
}
async unbanUsers (users: User[]) {
const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length })
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.userService.getUsers(this.pagination, this.sort)
this.selectedUsers = []
this.userService.getUsers(this.pagination, this.sort, this.search)
.subscribe(
resultList => {
this.users = resultList.data
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

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

View File

@ -0,0 +1,7 @@
@import '_variables';
@import '_mixins';
.unblock-button {
@include peertube-button;
@include grey-button;
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
@import '_variables';
@import '_mixins';
.unblock-button {
@include peertube-button;
@include grey-button;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
export * from './my-account-notification-preferences.component'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,10 +15,19 @@
</div>
</div>
<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>

View File

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

View File

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

View File

@ -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' ])

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
@import '_mixins';
.create-button {
@include create-button('../../../assets/images/global/add.svg');
@include create-button;
}
/deep/ .action-button {

View File

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