diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4b352922e..b5d7ede72 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -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). diff --git a/.gitignore b/.gitignore index 22478c444..a31da70a9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ /test4/ /test5/ /test6/ +/server/tests/fixtures/video_high_bitrate_1080p.mp4 # Production /storage/ diff --git a/.travis.yml b/.travis.yml index 9fd54447c..d252ae625 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,10 @@ addons: - g++-4.9 postgresql: "9.4" -cache: yarn +cache: + directories: + - $HOME/.cache/yarn + - $HOME/fixtures sudo: false @@ -39,17 +42,18 @@ matrix: - env: TEST_SUITE=api-1 - env: TEST_SUITE=api-2 - env: TEST_SUITE=api-3 + - env: TEST_SUITE=api-4 - env: TEST_SUITE=cli - env: TEST_SUITE=lint - env: TEST_SUITE=jest script: - - travis_retry npm run travis -- "$TEST_SUITE" + - NODE_PENDING_JOB_WAIT=1000 travis_retry npm run travis -- "$TEST_SUITE" after_failure: - - cat test1/logs/all-logs.log - - cat test2/logs/all-logs.log - - cat test3/logs/all-logs.log - - cat test4/logs/all-logs.log - - cat test5/logs/all-logs.log - - cat test6/logs/all-logs.log + - cat test1/logs/peertube.log + - cat test2/logs/peertube.log + - cat test3/logs/peertube.log + - cat test4/logs/peertube.log + - cat test5/logs/peertube.log + - cat test6/logs/peertube.log diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 160d6fc4f..f3254d2d6 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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" diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6007b4c..13bec7535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,317 @@ # Changelog +## v1.2.0 + +### BREAKING CHANGES + + * **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir)) + * **Docker:** Check you have all the storage fields in your `/config/production.yaml` file: https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/production.yaml#L34 + * **nginx:** Add redundancy endpoint in static file. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx** + * **nginx:** Add socket io endpoint. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx** + * Moderators can manage users now (add/delete/update/block) + * Add `tmp` and `redundancy` directories in configuration file. **You should configure them in your production.yaml** + +### Maintenance + + * Check free storage before upgrading in upgrade script ([@Nutomic](https://github.com/nutomic)) + * Explain that PeerTube must be stopped in prune storage script + * Add some security directives in the systemd unit configuration file ([@rigelk](https://github.com/rigelk) & [@mkoppmann](https://github.com/mkoppmann)) + * Update FreeBSD startup script ([@gegeweb](https://github.com/gegeweb)) + +### Docker + + * Patch docker entrypoint to speed up the chown at startup ([LecygneNoir](https://github.com/LecygneNoir)) + +### Features + + * Add Russian, Polish and Italian languages + * Add user notifications: + * Notification types: + * Comment on my video + * New video from my subscriptions + * New video abuses (for moderators) + * Blacklist/Unblacklist on my video + * Video import finished (error or success) + * Pending video published (after transcoding or a scheduled update) + * My account or one of my channel has a new follower + * Someone (except muted accounts) mentioned me in comments + * A user registered on the instance (for moderators) + * Notification actions: + * Add a web notification + * Send an english email + * Add contact form in about page (**enabled by default**) + * Add ability to unfederate a local video in blacklist modal (**checkbox checked by default**) + * Support additional video extensions if transcoding is enabled (**enabled by default**) + * Redirect to the last url on login + * Add ability to automatically set the video caption in URL. Example: https://peertube2.cpy.re/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d?subtitle=ru + * Automatically enable the last selected caption when watching a video + * Add ability to disable, clear and list user videos history + * Add a button to help to translate peertube + * Add text in the report modal to explain to whom the report will be sent + * Open my account menu entries on hover + * Explain what features are enabled on the instance in the about page + * Add an error message in the forgot password modal if the instance email system is not configured + * Add sitemap + * Add well known url to change password ([@rigelk](https://github.com/rigelk)) + * Remove 8GB video upload limit on client side. There may still be such limit depending on the reverse proxy configuration ([@scanlime](https://github.com/scanlime)) + * Add CSP ([@rigelk](https://github.com/rigelk) & [@Nutomic](https://github.com/nutomic)) + * Update title and description HTML tags when rendering video HTML page + * Add webfinger support for remote follows ([@acid-chicken](https://github.com/acid-chicken)) + * Add tooltip to explain how the trending algorithm works ([@auberanger](https://github.com/auberanger)) + * Warn users when they want to delete a channel because they will not be able to create another channel with the same name + * Warn users when they leave the video upload/update (on page refresh/tab close) + * Set max user name, user display name, channel name and channel display name lengths to 50 characters ([@McFlat](https://github.com/mcflat)) + * Increase video abuse length to 3000 characters + * Add totalLocalVideoFilesSize in the stats endpoint + +## Bug fixes + + * Fix the addition of captions to a video + * Fix federation of some videos + * Fix NSFW blur on search + * Add error message when trying to upload .ass subtitles + * Fix default homepage in the progressive web application + * Don't crash on queue error + * Fix EXDEV errors if you have multiple mount points + * Fix broken audio in transcoding with some videos + * Fix crash on getVideoFileStream issue + * Fix followers search + * Remove trailing `/` in CLI import script ([@HesioZ](https://github.com/HesioZ/)) + * Use origin video url in canonical tag + * Fix captions in HTTP fallback + * Automatically refresh remote actors to fix deleted remote actors that are still displayed on some instances + * Add missing translations in video embed page + * Fix some styling issues in dark mode + * Fix transcoding issues with some videos + * Fix Mac OS mkv/avi upload + * Fix menu overflow on mobile + * Fix ownership button icons ([@joshmorel](https://github.com/joshmorel)) + + +## v1.1.0 + +***Since v1.0.1*** + +### BREAKING CHANGES + + * **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir)) + +### Maintenance + + * Improve REST API documentation: https://docs.joinpeertube.org/api.html ([@rigelk](https://github.com/rigelk)) + * Add basic ActivityPub documentation: https://docs.joinpeertube.org/lang/en/devdocs/federation.html ([@rigelk](https://github.com/rigelk)) + * Add CLI option to run PeerTube without client ([@rigelk](https://github.com/rigelk)) + * Add manpage to peertube CLI ([@rigelk](https://github.com/rigelk)) + * Make backups of files in optimize-old-videos script ([@Nutomic](https://github.com/nutomic)) + * Allow peertube-import-videos.ts CLI script to run concurrently ([@McFlat](https://github.com/mcflat)) + +### Scripts + + * Use DB information from config/production.yaml in upgrade script ([@ldidry](https://github.com/ldidry)) + * Add REPL script ([@McFlat](https://github.com/mcflat)) + +### Docker + + * Add search and import settings env settings env variables ([@kaiyou](https://github.com/kaiyou)) + * Add docker dev image ([@am97](https://github.com/am97)) + * Improve docker compose template ([@Nutomic](https://github.com/nutomic)) + * Add postfix image + * Redirect HTTP -> HTTPS + * Disable Træfik web UI + +### Features + + * Automatically resume videos if the user is logged in + * Hide automatically the menu when the window is resized ([@BO41](https://github.com/BO41)) + * Remove confirm modal for JavaScript/CSS injection ([@scanlime](https://github.com/scanlime)) + * Set bitrate limits for transcoding ([@Nutomic](https://github.com/nutomic)) + * Add moderation tools in the account page + * Add bulk actions in users table (Delete/Ban for now) + * Add search filter in admin users table + * Add search filter in admin following + * Add search filter in admin followers + * Add ability to list all local videos + * Add ability for users to mute an account or an instance + * Add ability for administrators to mute an account or an instance + * Rename "News" category to "News & Politics" ([@daker](https://github.com/daker)) + * Add explicit error message when changing video ownership ([@lucas-dclrcq](https://github.com/lucas-dclrcq)) + * Improve description of the HTTP video import feature ([@rigelk](https://github.com/rigelk)) + * Set shorter keyframe interval for transcoding (2 seconds) ([@Nutomic](https://github.com/nutomic)) + * Add ability to disable webtorrent (as a user) ([@rigelk](https://github.com/rigelk)) + * Make abuse-delete clearer ([@barbeque](https://github.com/barbeque)) + * Adding minimum signup age conforming to ceiling GPDR age ([@rigelk](https://github.com/rigelk)) + * Feature/description support fields length 1000 ([@McFlat](https://github.com/mcflat)) + * Add background effect to activated menu entry + * Improve video upload error handling + * Improve message visibility on signup + * Auto login user on signup if email verification is disabled + * Speed up PeerTube startup (in particular the first one) + * Delete invalid or deleted remote videos + * Add ability to admin to set email as verified ([@joshmorel](https://github.com/joshmorel)) + * Add separators in user moderation dropdown + +### Bug fixes + + * AP mimeType -> mediaType + * PeerTube is not in beta anymore + * PeerTube is not in alpha anymore :p + * Fix optimize old videos script + * Check follow constraints when getting a video + * Fix application-config initialization in CLI tools ([Yetangitu](https://github.com/Yetangitu)) + * Fix video pixel format compatibility (using yuv420p) ([@rigelk](https://github.com/rigelk)) + * Fix video `state` AP context ([tcitworld](https://github.com/tcitworld)) + * Fix Linked Signature compatibility + * Fix AP collections pagination + * Fix too big thumbnails (when using URL import) + * Do not host remote AP objects: use redirection instead + * Fix video miniature with a long name + * Fix video views inconsistencies inside the federation + * Fix video embed in Wordpress Gutenberg + * Fix video channel videos url when scrolling + * Fix player progress bar/seeking when changing resolution + * Fix search tab title with no search + * Fix YouTube video import with some videos + +***Since v1.1.0-rc.1*** + +### Bug fixes + + * Fix AP infinite redirection + * Fix trending page + + +## v1.1.0-rc.1 (since v1.1.0-alpha.2) + +### Maintenance + + * Improve REST API documentation: https://docs.joinpeertube.org/api.html ([@rigelk](https://github.com/rigelk)) + * Add basic ActivityPub documentation: https://docs.joinpeertube.org/lang/en/devdocs/federation.html ([@rigelk](https://github.com/rigelk)) + * Add CLI option to run PeerTube without client ([@rigelk](https://github.com/rigelk)) + * Add manpage to peertube CLI ([@rigelk](https://github.com/rigelk)) + * Make backups of files in optimize-old-videos script ([@Nutomic](https://github.com/nutomic)) + * Allow peertube-import-videos.ts CLI script to run concurrently ([@McFlat](https://github.com/mcflat)) + +### Docker + + * Improve docker compose template ([@Nutomic](https://github.com/nutomic)) + * Add postfix image + * Redirect HTTP -> HTTPS + * Disable Træfik web UI + * Add ability to set an array in `PEERTUBE_TRUST_PROXY` ([LecygneNoir](https://github.com/LecygneNoir)) + +### Features + + * Add background effect to activated menu entry + * Improve video upload error handling + * Improve message visibility on signup + * Auto login user on signup if email verification is disabled + * Speed up PeerTube startup (in particular the first one) + * Delete invalid or deleted remote videos + * Add ability to admin to set email as verified ([@joshmorel](https://github.com/joshmorel)) + * Add separators in user moderation dropdown + +### Bug fixes + + * Check follow constraints when getting a video + * Fix application-config initialization in CLI tools ([Yetangitu](https://github.com/Yetangitu)) + * Fix video pixel format compatibility (using yuv420p) ([@rigelk](https://github.com/rigelk)) + * Fix video `state` AP context ([tcitworld](https://github.com/tcitworld)) + * Fix Linked Signature compatibility + * Fix AP collections pagination + * Fix too big thumbnails (when using URL import) + * Do not host remote AP objects: use redirection instead + * Fix video miniature with a long name + * Fix video views inconsistencies inside the federation + * Fix video embed in Wordpress Gutenberg + * Fix video channel videos url when scrolling + * Fix player progress bar/seeking when changing resolution + * Fix search tab title with no search + * Fix YouTube video import with some videos + + +## v1.1.0-alpha.2 (since v1.1.0-alpha.1) + +### Security/Maintenance/Federation + + * Add HTTP Signature in addition to Linked Signature: + * It's faster + * Will allow us to use RSA Signature 2018 in the future without too much incompatibilities in the peertube federation + +### Features + + * Set shorter keyframe interval for transcoding (2 seconds) ([@Nutomic](https://github.com/nutomic)) + * Add ability to disable webtorrent (as a user) ([@rigelk](https://github.com/rigelk)) + * Make abuse-delete clearer ([@barbeque](https://github.com/barbeque)) + * Adding minimum signup age conforming to ceiling GPDR age ([@rigelk](https://github.com/rigelk)) + * Feature/description support fields length 1000 ([@McFlat](https://github.com/mcflat)) + +### Bug fixes + + * Scale bitrate linearly with FPS ([@Nutomic](https://github.com/nutomic)) + * AP mimeType -> mediaType + * PeerTube is not in beta anymore + * PeerTube is not in alpha anymore :p + * Fix optimize old videos script + + +## v1.0.1 + +### Security/Maintenance/Federation + + * Add HTTP Signature in addition to Linked Signature: + * It's faster + * Will allow us to use RSA Signature 2018 in the future without too much incompatibilities in the peertube federation + + +## v1.1.0-alpha.1 + +We released this alpha version because some admins/users need some moderation tools we implemented in recent weeks. +This release could contain bugs. Don't expect a stable v1.1.0 until December :) + +### Scripts + + * Use DB information from config/production.yaml in upgrade script ([@ldidry](https://github.com/ldidry)) + * Add REPL script ([@McFlat](https://github.com/mcflat)) + +### Docker + + * Add search and import settings env settings env variables ([@kaiyou](https://github.com/kaiyou)) + * Add docker dev image ([@am97](https://github.com/am97)) + +### Features + + * Automatically resume videos if the user is logged in + * Hide automatically the menu when the window is resized ([@BO41](https://github.com/BO41)) + * Remove confirm modal for JavaScript/CSS injection ([@scanlime](https://github.com/scanlime)) + * Set bitrate limits for transcoding ([@Nutomic](https://github.com/nutomic)) + * Add moderation tools in the account page + * Add bulk actions in users table (Delete/Ban for now) + * Add search filter in admin users table + * Add search filter in admin following + * Add search filter in admin followers + * Add ability to list all local videos + * Add ability for users to mute an account or an instance + * Add ability for administrators to mute an account or an instance + * Rename "News" category to "News & Politics" ([@daker](https://github.com/daker)) + * Add explicit error message when changing video ownership ([@lucas-dclrcq](https://github.com/lucas-dclrcq)) + * Improve description of the HTTP video import feature ([@rigelk](https://github.com/rigelk)) + + +## v1.0.0 + +### SECURITY + + * Add more headers to HTTP signature to avoid actor impersonation by replaying modified signed HTTP requests (thanks Thibaut Girka) + +### Bug fixes + + * Check video exists before extending expiration + * Correctly delete redundancy files + * Fix account URI in remote comment modal ([@rigelk](https://github.com/rigelk)) + * Fix avatar update + * Avoid old issue regarding duplicated hosts in database + + ## v1.0.0-rc.2 ### Bug fixes @@ -87,7 +399,7 @@ ### Features - * Video redundancy system (experimental, see [the doc](/support/doc/redundancy.md)) + * Video redundancy system (experimental, see [the doc](https://docs.joinpeertube.org/lang/en/devdocs/architecture.html#redundancy-between-instances)) * Add peertube script (see [the doc](/support/doc/tools.md#cli-wrapper)) ([@rigelk](https://github.com/rigelk)) * Improve download modal ([@rigelk](https://github.com/rigelk)) * Add redirect after login ([@BO41](https://github.com/BO41)) diff --git a/CREDITS.md b/CREDITS.md index e0e647dc9..509f9800d 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -3,65 +3,82 @@ * [Chocobozzz](https://github.com/Chocobozzz) * [rigelk](https://github.com/rigelk) * [gegeweb](https://github.com/gegeweb) + * [Nutomic](https://github.com/Nutomic) * [Jorropo](https://github.com/Jorropo) + * [BO41](https://github.com/BO41) + * [joshmorel](https://github.com/joshmorel) + * [buoyantair](https://github.com/buoyantair) * [bnjbvr](https://github.com/bnjbvr) * [DavidLibeau](https://github.com/DavidLibeau) * [jankeromnes](https://github.com/jankeromnes) + * [JohnXLivingston](https://github.com/JohnXLivingston) + * [kaiyou](https://github.com/kaiyou) + * [McFlat](https://github.com/McFlat) * [DimitriGilbert](https://github.com/DimitriGilbert) * [floSoX](https://github.com/floSoX) * [Green-Star](https://github.com/Green-Star) - * [joshmorel](https://github.com/joshmorel) + * [thomaskuntzz](https://github.com/thomaskuntzz) * [rezonant](https://github.com/rezonant) - * [kaiyou](https://github.com/kaiyou) - * [Nutomic](https://github.com/Nutomic) - * [JohnXLivingston](https://github.com/JohnXLivingston) - * [okhin](https://github.com/okhin) - * [fflorent](https://github.com/fflorent) * [ldidry](https://github.com/ldidry) + * [okhin](https://github.com/okhin) + * [daftaupe](https://github.com/daftaupe) + * [LecygneNoir](https://github.com/LecygneNoir) + * [fflorent](https://github.com/fflorent) * [dedesite](https://github.com/dedesite) * [Nautigsam](https://github.com/Nautigsam) - * [BO41](https://github.com/BO41) - * [daftaupe](https://github.com/daftaupe) + * [scanlime](https://github.com/scanlime) + * [tcitworld](https://github.com/tcitworld) + * [am97](https://github.com/am97) * [dadall](https://github.com/dadall) * [jonathanraes](https://github.com/jonathanraes) - * [LecygneNoir](https://github.com/LecygneNoir) + * [anoadragon453](https://github.com/anoadragon453) * [rhaamo](https://github.com/rhaamo) * [mrflos](https://github.com/mrflos) * [jocelynj](https://github.com/jocelynj) + * [lucas-dclrcq](https://github.com/lucas-dclrcq) * [lucaspontoexe](https://github.com/lucaspontoexe) * [flyingrub](https://github.com/flyingrub) - * [tcitworld](https://github.com/tcitworld) + * [SerCom-KC](https://github.com/SerCom-KC) * [valvin1](https://github.com/valvin1) - * [am97](https://github.com/am97) * [taziden](https://github.com/taziden) * [sticmac](https://github.com/sticmac) + * [barbeque](https://github.com/barbeque) * [luzpaz](https://github.com/luzpaz) + * [acid-chicken](https://github.com/acid-chicken) * [louistio](https://github.com/louistio) * [qsypoq](https://github.com/qsypoq) + * [daker](https://github.com/daker) + * [xyproto](https://github.com/xyproto) * [Anton-Latukha](https://github.com/Anton-Latukha) * [noplanman](https://github.com/noplanman) + * [auberanger](https://github.com/auberanger) * [austinheap](https://github.com/austinheap) * [benabbottnz](https://github.com/benabbottnz) * [ewft](https://github.com/ewft) * [bradsk88](https://github.com/bradsk88) - * [WildYorkies](https://github.com/WildYorkies) * [Ealhad](https://github.com/Ealhad) + * [clementbrizard](https://github.com/clementbrizard) * [DeeJayBro](https://github.com/DeeJayBro) * [Edznux](https://github.com/Edznux) * [ebrehault](https://github.com/ebrehault) * [DatBewar](https://github.com/DatBewar) * [ReK2Fernandez](https://github.com/ReK2Fernandez) + * [Yetangitu](https://github.com/Yetangitu) * [grizio](https://github.com/grizio) * [Glandos](https://github.com/Glandos) * [lanodan](https://github.com/lanodan) + * [HesioZ](https://github.com/HesioZ) + * [jagannathBhat](https://github.com/jagannathBhat) * [jlebras](https://github.com/jlebras) * [alcalyn](https://github.com/alcalyn) * [mkody](https://github.com/mkody) - * [lucas-dclrcq](https://github.com/lucas-dclrcq) + * [pichouk](https://github.com/pichouk) * [zapashcanon](https://github.com/zapashcanon) * [mart-e](https://github.com/mart-e) - * [scanlime](https://github.com/scanlime) + * [0mp](https://github.com/0mp) + * [mkoppmann](https://github.com/mkoppmann) * [1000i100](https://github.com/1000i100) + * [roipoussiere](https://github.com/roipoussiere) * [zeograd](https://github.com/zeograd) * [PhieF](https://github.com/PhieF) * [Quenty31](https://github.com/Quenty31) @@ -77,6 +94,7 @@ * [imbsky](https://github.com/imbsky) * [ctlaltdefeat](https://github.com/ctlaltdefeat) * [jomo](https://github.com/jomo) + * [lsde](https://github.com/lsde) * [memoryboxes](https://github.com/memoryboxes) * [norrist](https://github.com/norrist) * [osauzet](https://github.com/osauzet) @@ -88,11 +106,13 @@ # Translations + * [abdhessuk](https://trad.framasoft.org/zanata/profile/view/abdhessuk) * [abidin24](https://trad.framasoft.org/zanata/profile/view/abidin24) * [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo) * [alice](https://trad.framasoft.org/zanata/profile/view/alice) * [anastasia](https://trad.framasoft.org/zanata/profile/view/anastasia) * [autom](https://trad.framasoft.org/zanata/profile/view/autom) + * [balaji](https://trad.framasoft.org/zanata/profile/view/balaji) * [bristow](https://trad.framasoft.org/zanata/profile/view/bristow) * [butterflyoffire](https://trad.framasoft.org/zanata/profile/view/butterflyoffire) * [chocobozzz](https://trad.framasoft.org/zanata/profile/view/chocobozzz) @@ -103,22 +123,30 @@ * [ehsaan](https://trad.framasoft.org/zanata/profile/view/ehsaan) * [esoforte](https://trad.framasoft.org/zanata/profile/view/esoforte) * [fkohrt](https://trad.framasoft.org/zanata/profile/view/fkohrt) + * [giqtaqisi](https://trad.framasoft.org/zanata/profile/view/giqtaqisi) * [goofy](https://trad.framasoft.org/zanata/profile/view/goofy) * [gorkaazk](https://trad.framasoft.org/zanata/profile/view/gorkaazk) * [gwendald](https://trad.framasoft.org/zanata/profile/view/gwendald) * [h3zjp](https://trad.framasoft.org/zanata/profile/view/h3zjp) + * [jfblanc](https://trad.framasoft.org/zanata/profile/view/jfblanc) * [jhertel](https://trad.framasoft.org/zanata/profile/view/jhertel) + * [jmf](https://trad.framasoft.org/zanata/profile/view/jmf) * [jorropo](https://trad.framasoft.org/zanata/profile/view/jorropo) + * [kairozen](https://trad.framasoft.org/zanata/profile/view/kairozen) * [kedemferre](https://trad.framasoft.org/zanata/profile/view/kedemferre) * [kousha](https://trad.framasoft.org/zanata/profile/view/kousha) * [krkk](https://trad.framasoft.org/zanata/profile/view/krkk) + * [landrok](https://trad.framasoft.org/zanata/profile/view/landrok) + * [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48) * [m4sk1n](https://trad.framasoft.org/zanata/profile/view/m4sk1n) * [matograine](https://trad.framasoft.org/zanata/profile/view/matograine) + * [medow](https://trad.framasoft.org/zanata/profile/view/medow) * [mhu](https://trad.framasoft.org/zanata/profile/view/mhu) * [midgard](https://trad.framasoft.org/zanata/profile/view/midgard) * [nbrucy](https://trad.framasoft.org/zanata/profile/view/nbrucy) * [nitai](https://trad.framasoft.org/zanata/profile/view/nitai) * [noncommutativegeo](https://trad.framasoft.org/zanata/profile/view/noncommutativegeo) + * [nopsidy](https://trad.framasoft.org/zanata/profile/view/nopsidy) * [nvivant](https://trad.framasoft.org/zanata/profile/view/nvivant) * [osoitz](https://trad.framasoft.org/zanata/profile/view/osoitz) * [outloudvi](https://trad.framasoft.org/zanata/profile/view/outloudvi) @@ -129,6 +157,7 @@ * [s8321414](https://trad.framasoft.org/zanata/profile/view/s8321414) * [sato_ss](https://trad.framasoft.org/zanata/profile/view/sato_ss) * [sercom_kc](https://trad.framasoft.org/zanata/profile/view/sercom_kc) + * [severo](https://trad.framasoft.org/zanata/profile/view/severo) * [silkevicious](https://trad.framasoft.org/zanata/profile/view/silkevicious) * [sosha](https://trad.framasoft.org/zanata/profile/view/sosha) * [spla](https://trad.framasoft.org/zanata/profile/view/spla) @@ -139,11 +168,17 @@ * [thibaultmartin](https://trad.framasoft.org/zanata/profile/view/thibaultmartin) * [tirifto](https://trad.framasoft.org/zanata/profile/view/tirifto) * [tuxayo](https://trad.framasoft.org/zanata/profile/view/tuxayo) + * [unextro](https://trad.framasoft.org/zanata/profile/view/unextro) * [unzarida](https://trad.framasoft.org/zanata/profile/view/unzarida) + * [vincent](https://trad.framasoft.org/zanata/profile/view/vincent) * [wanhua](https://trad.framasoft.org/zanata/profile/view/wanhua) * [xinayder](https://trad.framasoft.org/zanata/profile/view/xinayder) * [xosem](https://trad.framasoft.org/zanata/profile/view/xosem) * [zveryok](https://trad.framasoft.org/zanata/profile/view/zveryok) + * [aditoo](https://trad.framasoft.org/zanata/profile/view/aditoo) + * [autom](https://trad.framasoft.org/zanata/profile/view/autom) + * [curupira](https://trad.framasoft.org/zanata/profile/view/curupira) + * [leeroyepold48](https://trad.framasoft.org/zanata/profile/view/leeroyepold48) # Design diff --git a/FAQ.md b/FAQ.md index dba6bb1d0..e335868f8 100644 --- a/FAQ.md +++ b/FAQ.md @@ -5,6 +5,7 @@ +- [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) +## 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) diff --git a/README.md b/README.md index c3a39eb1d..a9b4eb54a 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,29 @@

- PeerTube - + PeerTube +

Website | Join an instance - | Create one + | Create an instance | Chat with us + | Donate

-Federated (ActivityPub) video streaming platform using P2P (BitTorrent) -directly in the web browser with WebTorrent. +Be part of a network of multiple small federated, interoperable video hosting providers. Follow video creators and create videos. No vendor lock-in. All on a platform that is community-owned and ad-free.

-We have run a crowdfunding campaign to pave the road to version 1.0 of PeerTube. Thanks to everyone who pitched in and shared the news around. You can now check out the corresponding milestone and help its development! + Developed with ❤ by Framasoft +

+ +

+ + Framasoft logo +

@@ -59,11 +65,25 @@ directly in the web browser with

- + screenshot

+Introduction +---------------------------------------------------------------- + +PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper: But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers, all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse (federated video network) by talking our implementation of ActivityPub. Video load is reduced thanks to P2P (BitTorrent) in the web browser via WebTorrent. + +To learn more, see: +* This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works +* PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org) +* Demonstration instances: + * [peertube.cpy.re](https://peertube.cpy.re) + * [peertube2.cpy.re](https://peertube2.cpy.re) + * [peertube3.cpy.re](https://peertube3.cpy.re) +* This [video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) demonstrating the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) (a decentralized Twitter alternative) + :sparkles: Features ---------------------------------------------------------------- @@ -79,7 +99,7 @@ Just upload your videos, and be sure they will stream anywhere. Add a descriptio

Keep in touch with video creators

-Follow your favorite channels from PeerTube or really any other place. No need to have an account on the instance you watched a video to follow its author, you can do all of that from the Fediverse (Mastodon, Pleroma and plenty others), or just with good ol' RSS. +Follow your favorite channels from PeerTube or really any other place. No need to have an account on the instance you watched a video to follow its author, you can do all of that from the Fediverse (Mastodon, Pleroma, and plenty others), or just with good ol' RSS.

--- @@ -95,38 +115,29 @@ Be it as a user or an instance administrator, you can decide what your experienc

Communities that help each other

-In addition to visitors using WebTorrent to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our redundancy guide). +In addition to visitors using WebTorrent to share the load among them, instances can help each other by caching one another's videos. This way even small instances have a way to show content to a wider audience, as they will be shouldered by friend instances (more about that in our redundancy guide).

-Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisments that hurt visitors and incentivize alter creativity (more about that in our FAQ). +Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and incentivize alter creativity (more about that in our FAQ).

---- - -Want to see it in action? - - * Demonstration servers: - * [peertube.cpy.re](https://peertube.cpy.re) - * [peertube2.cpy.re](https://peertube2.cpy.re) - * [peertube3.cpy.re](https://peertube3.cpy.re) - * [Video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) What is PeerTube? - * [Video](https://peertube.cpy.re/videos/watch/f78a97f8-a142-4ce1-a5bd-154bf9386504) - to see what the "decentralization feature" looks like - * [Video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) to see - the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) - -:question: Motivation +:raised_hands: Contributing ---------------------------------------------------------------- -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. +You don't need to be a coder to help! -So we need to have a decentralized network of servers seeding videos (as -[Diaspora](https://github.com/diaspora/diaspora) for example). 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 make BitTorrent inside the web browser, as of today. +You can give us your feedback, report bugs, help us translate PeerTube, write documentation, and more. Check out the [contributing +guide](/.github/CONTRIBUTING.md) to know how, it takes less than 2 minutes to get started. :wink: + +You can also join the cheerful bunch that makes our community: + +* Chat: + * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** + * Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)** +* Forum: + * Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube) + +Feel free to reach out if you have any questions or ideas! :speech_balloon: :package: Create your own instance ---------------------------------------------------------------- @@ -144,20 +155,6 @@ See the [production guide](/support/doc/production.md), which is the recommended See the [community packages](https://docs.joinpeertube.org/lang/en/docs/install.html), which cover various platforms (including [YunoHost](https://install-app.yunohost.org/?app=peertube) and [Docker](/support/doc/docker.md)). -:wrench: Contribute/Translate/Test ----------------------------------------------------------------- - -*Spoiler alert*: you don't need to be a coder to help! - -See the [contributing -guide](/.github/CONTRIBUTING.md). Or simply join the cheerful bunch that makes our community: - - * Chat: - * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)** - * Matrix (bridged on the IRC channel) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)** - * Forum: - * Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube) - :book: Documentation ---------------------------------------------------------------- @@ -177,44 +174,23 @@ See the more general [admin documentation](https://docs.joinpeertube.org/lang/en * [Import videos (YouTube, Dailymotion, Vimeo...)](/support/doc/tools.md) * [Upload videos from the CLI](/support/doc/tools.md) - * [Admin server tools (create transcoding jobs, prune storage...)](/develop/support/doc/tools.md#server-tools) + * [Admin server tools (create transcoding jobs, prune storage...)](/support/doc/tools.md#server-tools) ### Technical documentation -See [ARCHITECTURE.md](/ARCHITECTURE.md) for a more detailed explanation of the architectural choices. +See the [architecture blueprint](https://docs.joinpeertube.org/lang/en/devdocs/architecture.html) for a more detailed explanation of the architectural choices. -#### Backend - - * REST API: - * Quick Start: [/support/doc/api/quickstart.md](/support/doc/api/quickstart.md) - * Swagger/OpenAPI schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml) - * HTML explorer: [/support/doc/api/html/index.html](https://htmlpreview.github.io/?https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/api/html/index.html) - * Servers communicate with each other with [Activity - Pub](https://www.w3.org/TR/activitypub/). - * Each server has its own users who query it (search videos, query where the - torrent URI of this specific video is...). - * When a user uploads a video, the server sends its followers metadata about the video (name, short description, torrent URI...). - * A server is a tracker responsible for all the videos uploaded on it. - * Even if nobody watches a video, it is seeded by the server (through - [WebSeed protocol](http://www.bittorrent.org/beps/bep_0019.html)) where the - video was uploaded. - -Here are some simple schemes: - -

- -Decentralized - -Watch a video - -Watch a P2P video - -

+See our REST API documentation: + * OpenAPI 3.0.0 schema: [/support/doc/api/openapi.yaml](/support/doc/api/openapi.yaml) + * Spec explorer: [docs.joinpeertube.org/api.html](http://docs.joinpeertube.org/api.html) +See our [ActivityPub documentation](https://docs.joinpeertube.org/lang/en/devdocs/federation.html). :heart: Supports of our crowdfunding ---------------------------------------------------------------- +We have run [a crowdfunding campaign](https://www.kisskissbankbank.com/en/projects/peertube-a-free-and-federated-video-platform) to pave the road to the version 1.0.0 of PeerTube. Thanks to everyone who pitched in and shared the news around! + Quonfucius, IP Solution, \_Laure\_, @lex666, 0x010C, 3dsman, 3rw4n-G3D, aallrd, Abel-Berger, Adam-Odell, adechambost, adim, adngdb, Adrien Thurotte, Adrien-BARAN, Adrien-Hamraoui, Adrien-Horcholle, Adrien-Luxey, Adrien-Polar, Adrien-Touminet, Agathe Begault, Agence-Différente, Ahmed-Al-Ahmed, aiprole, akpoptro, Al-Nimr, Alain-Delgrange, Alain-Fyon, Alain-Girard, Alain-MICHEL, Aleksandar-Aleksandrov, Alex-Chancellé, Alex-Dufournet, Alex-Gleason, Alexander-Murray-Watters, Alexandre-Alapetite, Alexandre-Badez, Alexandre-Giuliani, Alexandre-Mercier, Alexandre-Roux-2, Alexandre-SIMON, Alexandre29, Alexia-Monsavoir, Alexis-Frn, Alexis-Gros, Alexis-Kauffmann, alfajet, Alias, alinemont, Aliocha-Lang, Alllightlong, aloisdg, Amanda Hinault, André-Rabe, Anne-PROTAS, antoine, Antoine Derouin, Antoine-Beauvillain, Antoine-Deléron, antomoro, Antón López, Antonin-DENIS, Antonin-Segault, aokami, Apichat-Apichat, Ar-To, ARIAS-Frédéric-2, ariasuni, Aris-Papathéodorou, Arnaud -Vigoureux , Arnaud-Mounier, Arnaud-Risler, Arnaud-Vigouroux, Arnulf, Arthur-Bellier, arthur-bello, Arthur-Charron, Arthur-De Kimpe, Arthur.Ball, Arthur.Frin, Arvi-LEFEVRE, athanael.fr, auber38, Auguste Psqr, Aurélien-Tamisier, Avel-Musicavel, axel-guegant, Axel-Plat, Aymeric-Dlv, Ayst, Azenilion, Bandino, baptiste-lemoine, Baptiste-Rochez, baruica, Bastien-Dangin, batlab, bcourtine, Bea-Schaack-2, beaufils, beaumme, Belmont1, Ben-Geeraerts, Ben-Meijering, Benjamin-Baratta, Benjamin-Roussel, Benoît Joffre, Benoîtdd, Bernard-Legrand, Bernard-Vauquelin, Bernhard-Hayden, bertrand.arlabosse, bigsicret, bjg, bnjbvr, bob\_isat, bobstechsite, Bolton-Allan, Boov', Boris-ARGAUD, Brice.Francois, broz42, Bruno Lefèvre, Bruno-Douville, Bruno-Fortabat, Bruno-Gadaleta, Bruno-VASTA, Bumblebee, Butchcassidy, Cadiou-Christophe, calendros, Candy-Ming, cappitaine, Carmen-Drocourt, carrigns, case, Cathy-Barbet, CBach, ccazin, Cecile-Obernesser, Cecilia-:), Cédric-Bleschet, Cédric.Bayle, Cestdoncvrai, cgay, champ contrechamp, chapa, charlerlin, charles-jacquin, Charlie-Duclut, charlotte-cgondre78, Chris-Doe, chris-louba, Christel-Berthelot, Christian-FERRARIS, christiannavelot, Christophe-Bastin, christophe-beziers la fosse, Christophe-Pieret, Christophe-Verhaege, christophec, Christopher-Bero, chtfn, chud, Claire-C, clairezed, Claude-POUGHEON, Clément-Hubert, Clément-Morelle, clydeb, Comamanel, Côme Chilliet, Confederac.io, Consulting-AZAPTEC, Corentin3892, CryoGen, cyp, Cypher-Goat, Cyril, Cyril\_M\_, Cyril-MONMOUTON, Cyril-Waechter, Damien-Gabard, Damien-Garaud, Dams3132, Daniel Kuebler, Daniel Waxweiler, Daniel-Bartsch, Daniel-PIPALA, Daniel-Struck, Daniel-Thul, Danny-Joerger, DansLeRuSH, DantSu, Dany-Marcoux, Daouzli-Adel, Darfeld, Darth\_Judge, Dashcom, David-BADOIL, David-Benoist, David-Dormoy, David-Gil-2, David-Velasco, David-Wagner, David-writ, davlgd, davyg2, dbudo72300, de Folleville -Matthieu , DeBugs, Denis-Lecourtiller, Denis-Vannier, Desmu, Didier-Bove, Diego-Crespo, Dimitri-Stouney, dino, Dinosaure, Doc Skellington, Dominique-Brun, dr4Ke, DreamClassier, DRogueRonin, dussydelf, Dylan-Moonfire, Ealhad, Edouard-SCHWEISGUTH, Elanndelh--, ElodieEtJimmy, Éloi-Rivard, Elric-Noel, Elwan-Héry, Emilie-Wietzke, Emilien-Ghomi, eparth, Eric-Bouhana, Eric-Hendricks, Eric.Vales, Erwan-Moreau, Erzender, ESS\_Clem, Etienne-Baqué, Etienne-Botek, Etienne-Lmn, Ex-Serv, fabeveynes, Fabien BERINI ( Rehvaro ) , Fabien Freling, Fabien-Roualdes, Fabien.Abraini, Fabien.Bonneval, fabrice-simon, farlistener, Felix-ROBICHON, FelixDouet, FHE, Fiamoa-McBenson, flamwenco, Flopômpôm, FloraGC, Florent-Deschamps, Florent-Fayolle, Florent-Mallet, Florent-Vasseur, Florent.Duveau, Florestan Fournier, Florian Kohrt, Florian-Bellafont, Florian-Douay, Florian-LE GOFF, Florian-Siegenthaler, Florian.Freyss, fobrice, FOKUZA, Fol-De Dol, FP45, Francis.Moraud, François-Dambrine, François-Deguerry, Francois-Goer, François-Lecomte, François-Lemaire, François-Malterre, François-MORLET, François-Schoubben, François-Xavier-Davanne, François-Zajéga, francois.peyratout, Frathom, Fred-Fred-2, Frédéric GUÉLEN, Frédéric-Blumstein, Frédéric-Meurou, Frederic-Reynaud, Frédéric-Sagot, Frek, FrenchHope, freyja, FugazziPL, Funky-Whale, Gabriel-Devillers, Gabriel-Mirété, Galedas, GardoToF, Gaspard-Kemlin, GauthierPLM, Gauvain "GovanifY" Roussel-Tarbouriech, Gavy, gdquest, Geek Faëries, Geneviève-Perello, Geoffroy-MANAUD, Geojulien, Georges-Dutreix, Georges-Sempéré, Gerald-Vannier, Gérard-Brasquet, Gérard-Sensevy, Gerrit-Großkopf, GGBNM, Ghislain-Fabre, Gil-Felot, Gilles-Brossier, Gilles-Moisan, Gilles-SACLIER, Gilles-Trossevin, Gilou, GinGa, ginkgopr, glazzara, Glen-Lomax, Gof, Gonçalves-Daniel, goofy-goofy, grandlap, GRAP-Groupement Régional Alimentaire de Proximité, greg-chapuis, Grégoire-Delbeke, Grégory-Becq, Grégory-Goulaouic, Gregouw, Grizix, GrosCaillou, Grummfy, grumph, guiaug, Guillaume-Allart, Guillaume-Chambert, Guillaume-Chaslot, Guillaume-David, Guillaume-Duc, Guillaume-Gay, Guillaume-Lecoquierre, Guillaume007, guillaumefavre, Guiraud-Dominique, Guy-Torreilles, GwendalL, gwlolos, Hanna-E, Hanno-Wagner, Harald-Eilertsen, Harpocrate, Hebus82, Hellmut, Henri-ROS, hervelc, hguilbert, Hisham-Muhammad, Hoang-Mai-Lesaffre, Homerc, homosapienssapiens, hoper, Hoshin, Hugo-Lagouge, Hugo-SIMANCAS, Hugo-Simon, Hylm, IchbinRob, Ivan-Ogai, Ivan.D'halluin, Ivar-Troost, J-C-2, Jacques-Roos, James-Moore, James-Valleroy, Jan-Aagaard, Jan-Keromnes, Jancry, Janko-Mihelić, jano31coa, Jboot, jcgross, Jean CHARPENTIER, jean claude-skowron, Jean Dos, jean luc-PERROT, Jean-Baptiste-Maneyrol, Jean-charles-Surbayrole, Jean-claude-Jouanne, jean-dreyfus, jean-FISCHER, JEAN-FRANCOIS-BOUDEAU, Jean-Francois-Ducrot, Jean-François-PETITBON, Jean-François-Tomasi, Jean-Galland, Jean-louis-Bergamo, Jean-Luc-PIPO, Jean-Marie-Graïc, Jean-Martin Laval, Jean-Noel-Bruletout, Jean-Paul-GIBERT, Jean-Paul-Lescat, jean-philippe-bénétrix, Jean-Philippe-Eisenbarth, Jean-Philippe-Renaudet, Jean-Philippe-Rennard, Jean-Sébastien-Renaud, Jean-Yves Kiger, Jean-Yves-DUPARC, Jeanne-Corvellec, jeansebastien, Jelv, Jérémie -Wach, Jeremie-Lestel, Jérémy-Korwin, Jérôme-Avond, Jerome-Bu, Jerome-Denis, Jérôme-ISNARD, jerome-simonato, JeromeD, Jery, Jezza, Jim-McDoniel, jl-M-2, jlanca, jlcpuzzle, jn-m, jnthnctt, joakim.faiss, Joe-Riche, Joévin-SOULENQ, Johann-FONTAINE, John-Devor, John-Doe, Jojo-Boulix, Jonas-Aparicio, Jonathan-Dollé, Jonathan-Kohler, Jonathan-LAURENT, Jos-van den Oever, Joseph-Lawson, Jozef-Knaperek, jroger, ju, jubarbu, Julianoe-G, Julie-Bultez, Julien Loudet, Julien Maulny (alcalyn), Julien-AILHAUD, Julien-Aubin, Julien-Biaudet, Julien-Bréchet, Julien-Cochennec, Julien-Duroure, Julien-Huon, Julien-Lemaire, Julien-Weber, jyb, K-\_, KalambakA, Kanor, kari-kimber, Karim-Jouini, karl-bienfait, Kdecherf, Keplerpondorskell, kevin-Beranger, Kevin-Nguyen, King-Of Peons, Kioob, kloh, kokoklems, Konstantin-Kovar, Kriĉjo, Kyâne-PICHOU, L'elfe-Sylvain, La Gonz, Lara-Dufour, lareinedeselfes, Laurence-Giroud, laurent-fuentes, Laurent-HEINTZ, Laurent-PICQUENOT, ldubost, lebidibule, LeChi, LeDivinBueno, Legrave, Les Assortis, Leyokki-Tk, LibreEnFete-en Tregor, LilO. Moino, Liloumuloup, Linuxine-T, lionel-lachaud, Lionel-Schinckus, Loïc-L'Anton, Loïc.Guérin, Louis-Gatin, Louis-Marie-BAER, Louis-Rémi.Babé, Louis-Roche, Louisclement, Lu, ludovic-lainard, Ludovic-Pénet, Lukas-Steiblys, lusoheart, Mad Sugar, maguy-giorgi, mahen, maiido, Malphas, ManetteBE, Manon-Amalric, Manuel-Vazquez, ManuInzesky, Manumerique, Marc-BESSIERES, Marc-DUFOURNET, Marc-GASSER, Marc-Honnorat, marc-wilzius, marc.ribault.1, Marco-Heisig, Marie-PACHECO, Marien-Fressinaud, Marius-Lemonnier, Mark-O'Donovan, marliebo, marmat8951, mart1n, martensite, Mathdatech, Mathias-Bocquet, Mathieu-Amirault, Mathieu-B., Mathieu-Cornic, Mathieu-VIRAMAN, Matías-Pérez, Matilin-Torre, matt.faure, Mattéo-Delabre, Matthias-Devlamynck, Matthieu-Bollot, Matthieu-De Beule, Matthieu-DEVILLERS, Matthieu-Dupont de Dinechin, Matthieu-Gaudé, Matthieu-Sauboua-Beneluz, matthieublanco, MatthieuSchneider, Max-PENY, Maxime-de WYROW, Maxime-Desjardin, Maxime-Forest, maxime-haag, Maxime-Mangel, Maximilian Praeger, Mayeul-Cantan, Mayeul-Guiraud, mcg1712, metalvinze, Mewen, mheiber, Michael-Koppmann, Michael-Loew, Michael-Q. Bid, Michal-Herda, Michal-Noga, Michel-DUPONT, Michel-Le Lagadec, Michel-POUSSIER, Michel-Roux, Mickaël-Gauvin, Mickael-Liegard, MicMP3Man, Miguel-de la Cruz, Mike-Kasprzak, Mimon-Lapompe, Mister-Ocelot, mjhvc, Moutmout, MouTom, MP, mphdp, Mr-Tea, msellebulle, Mushussu, mylainos, nanouckd, Nasser-Debruyere, Nat-Tuck, Nathan.B, nayya, nazgulz666, Neal-Wilson, neeev, neodarz-neodarz, NepsKi, Nestorvep, NHenry, Nialix, NicoD, Nicolas-Auvray, nicolas-k, Nicolas-Pinault, Nicolas-Ruffel, NicolasCARPi, nicolaslegland, niconil, Niles, nitot, Nono1965, Norbert, Norde, Numcap, obergix, Obrow, Okki, Olivier-Calzi, Olivier-Ganneval, Olivier-Marouzé, Olivier-Mondoloni, olivier-pierret, Oncela-Petit Chat, Óskar-Sturluson, p3n15634n7, Paindesegle, Pas De-Panique, Pascal-BLEUSE, Pascal-Larramendy, Patrice-Jabeneau, patrice-maertens, patrick-bappel, PATRICK-GRANDIN, Patrick-MERCIER, Patrickl , Paul-Härle, Paul-Tardy, pbramy, Pedro-CADETE, Perrine-de Coëtlogon, Peter\_Fillgod, Petter-Joelson, Philippe-BATTMANN, Philippe-Cabaud, Philippe-Debar, philippe-giffard, Philippe-Lallemant, Philippe-Le Van, philippe-lhardy, Philippe-Thébault, Philippe-VINCENT-2, PhilOGM, Pierre 'catwell' Chapuis, Pierre Gros, Pierre-Antoine-Champin, Pierre-Bresson-2, Pierre-d'Alençon, Pierre-Equoy, Pierre-Girardeau, Pierre-Houmeau, Pierre-Marijon, Pierre-petch, Pierrick-Couturier, Pilou-CaraGk, Piotr-Miszczak, Pla, Plastic Yogi, PME2050, pmiossec, Pofilo, Polioman, Polios63, Poutchiny, PRALLET-Claude, PtrckVllnv, Pulov Yuran, queertube, Quentin-Dugne, Quentin-PAGÈS, ra-mon, Radhwan-Ben Madhkour, Raphaël-Brocq, Raphaël-Grolimund, Raphaël-Piédallu, raphane, Raphip, Raven, Raymond-Lutz, Razael, Rebecca-Breu, Remi-Durand, Rémi-Herrmann, Rémi-Verschelde, Remigho, Remix-the commons, Remy-Grauby, Rémy-Pradier, Renaud-Vincent, rgggn, rigelk, rip, Rivinbeg, Robert-Riemann, Robin Biechy, Roger-FRATTE, roipoussiere, Rolindes-Arroyo, Romain Théry-Hermain, Romain-Bouyé, Romain-Ortiz, RomainVENNE, Romuald-EYRAUD, royhome, Rudy-aparicio, Rusty-Dwyer, rverchere, sajous.net, Salah-ZERGUI, Sam-R, Samh, Samuel Tardieu, Samuel-FAYET, Samuel-Verschelde, Sanpi, Sascha-Brendel, Schwartz, Se7h, Sebastiaan-Glazenborg, Sebastian-Hugentobler, Sébastien Adam, Septie, Ser Eole, Severin-Suveren, severine-roger, shlagevuk-shlagevuk, Siegfried-Ehret, Simon-Hemery, Simon-Larcher, Simon-Reiser, Simounet, Siri-Louie, sissssou, skarab, Skurious, skynebula, Sohga-Sohga, Solène-Rapenne, solinux, Sophie-Imbach , Sosthen, Spiderweak, Stanislas-ANDRE, Stanislas-Michalak, starmatt, Steef, Stefan-Petrovski, Stéphane-Girardon, Stéphanie-Baltus, Stev-3d, Stoori, SuckyStrike, Sufflope, Sulfurax, SundownDEV, Swann-Fournial, Syk, Syluban, Sylv1c, Sylvain Bellone, Sylvain P, Sylvain\_M, Sylvain-Cazaux, Sylvain-GLAIZE, sylvain.arrachart, Sylvestre Ledru, sylvie-boutet, Sylvie-TORRES, tael67, tang35, tangi\_b, Tarulien, Taunya-Debolt, Tazimut-Khaelyor, terry-maire, Thanaen, Thatoo, Théophile-Noiré, Thibault-Vlieghe, Thierry-Chancé, Thierry-Fenasse, Thomas-Aurel, Thomas-CALVEZ, thomas-constans, Thomas-Kuntz, thomassin-loucas, Thosbk, ticosc, Tim-Albers, Tinapa -Itastri, TkPx, TM, tnntwister, TomR, Tomus, Tonio-Bilos, tony-carnide, Toover, toto-leroidelasaucisse, ToumToum, TP., trigrou, Tristan-Porteries, Tryph, Tursiops, tzilliox, U-&\_\`HbAAe4onnpN9!e+/#42\*5>k^E, Ulrich-Norbisrath, Un Sur Quatre, Valerio-Paladino, Valerio-Pilo, Valeryan\_24, Valou69, Vegattitude, Velome, Vergogne, Vero-Pajot, vianneyb, Victo-Sab, Victor -Hery, Victorien-Labalette, Vincent-Corrèze, Vincent-Fromentin, Vincent-Lamy, Vincent-Lasseur, VINCENT-PEYRET, vmorel, Walter-van Holst, Watsdesign, Wesley-Moore, williampolletdev, win100, wyk, Xaloc-Xaloc, Xavier ALT, Xavier-Chantry, Xavier-Godard, XoD, Yaaann, Yann-Delaunoy, Yann-Nave, yannick-grenzinger, yanselmetti, Ykatsot, Yohann-Bacha, yopox, Youen-Toupin, Yves-Caniou, Yves-Gerech, zar-rok, ZeBlackPearl, ZeGreg diff --git a/SECURITY.md b/SECURITY.md index 5c668a2a3..b80f8ad00 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,8 +1,6 @@ -**Introduction** - Security is core to our values, and we value the input of hackers acting in good faith to help us maintain a high standard for the security and privacy for our users. This includes encouraging responsible vulnerability research and disclosure. This policy sets out our definition of good faith in the context of finding and reporting vulnerabilities, as well as what you can expect from us in return. -**Expectations** +## Expectations When working with us according to this policy, you can expect us to: - Extend Safe Harbor (see below) for your vulnerability research that is related to this policy; @@ -10,7 +8,7 @@ When working with us according to this policy, you can expect us to: - Work to remediate discovered vulnerabilities in a timely manner; and - Recognize your contribution to improving our security if you are the first to report a unique vulnerability, and your report triggers a code or configuration change. -**Safe Harbor** +## Safe Harbor When conducting vulnerability research according to this policy, we consider this research to be: - Authorized in accordance with the law, and we will not initiate or support legal action against you for accidental, good faith violations of this policy; @@ -22,7 +20,7 @@ You are expected, as always, to comply with all applicable laws. If at any time you have concerns or are uncertain whether your security research is consistent with this policy, please submit a report through one of our Official Channels before going any further. -**Ground Rules** +## Ground Rules To encourage vulnerability research and to avoid any confusion between good-faith hacking and malicious attack, we ask that you: - Play by the rules. This includes following this policy, as well as any other relevant agreements. If there is any inconsistency between this policy and any other relevant terms, the terms of this policy will prevail. @@ -35,10 +33,15 @@ To encourage vulnerability research and to avoid any confusion between good-fait - You should only interact with test accounts you own or with explicit permission from the account holder. - Do not engage in extortion. -**Official Channels** +## Disclosure Terms + +The vulnerability is kept private until a majority of instances known on instances.joinpeertube.org have updated to a safe version of PeerTube or applied a hotfix. The PeerTube development team coordinates efforts to update once the patch is issued. + +## Official Channels To help us receive vulnerability submissions we use the following official reporting channels: - chocobozzz@cpy.re (GPG: [583A612D890159BE](https://keybase.io/chocobozzz/pgp_keys.asc?fingerprint=c44aad638367912ca93edd57583a612d890159be)) +- sendmemail@rigelk.eu (GPG: [EA12971B0E438F36](https://api.github.com/users/rigelk/gpg_keys)) If you think you have found a vulnerability, please include the following details with your report and be as descriptive as possible: - The location and nature of the vulnerability, diff --git a/client/e2e/src/po/video-watch.po.ts b/client/e2e/src/po/video-watch.po.ts index e17aebc29..d1e2a73b8 100644 --- a/client/e2e/src/po/video-watch.po.ts +++ b/client/e2e/src/po/video-watch.po.ts @@ -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) { diff --git a/client/package.json b/client/package.json index 76a4eedad..3eea661f1 100644 --- a/client/package.json +++ b/client/package.json @@ -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": "/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", diff --git a/client/src/app/+about/about-instance/about-instance.component.html b/client/src/app/+about/about-instance/about-instance.component.html index 5970cac01..8c700752e 100644 --- a/client/src/app/+about/about-instance/about-instance.component.html +++ b/client/src/app/+about/about-instance/about-instance.component.html @@ -1,39 +1,52 @@ -
- About {{ instanceName }} instance -
+
+
+
+
About {{ instanceName }} instance
-
-
{{ shortDescription }}
-
+
Contact administrator
+
-
-
Description
+
+
{{ shortDescription }}
+
-
-
+
+
Description
-
-
Terms
+
+
-
-
+
+
Terms
- -
- User registration is allowed and +
-
- User registration is currently not allowed. +
+ +
-
\ No newline at end of file +
+ + diff --git a/client/src/app/+about/about-instance/about-instance.component.scss b/client/src/app/+about/about-instance/about-instance.component.scss index b451e85aa..75cf57322 100644 --- a/client/src/app/+about/about-instance/about-instance.component.scss +++ b/client/src/app/+about/about-instance/about-instance.component.scss @@ -2,9 +2,19 @@ @import '_mixins'; .about-instance-title { - font-size: 20px; - font-weight: bold; - margin-bottom: 15px; + display: flex; + justify-content: space-between; + + & > div { + font-size: 20px; + font-weight: bold; + margin-bottom: 15px; + } + + & > .contact-admin { + @include peertube-button; + @include orange-button; + } } .section-title { diff --git a/client/src/app/+about/about-instance/about-instance.component.ts b/client/src/app/+about/about-instance/about-instance.component.ts index 354f52ce7..a1b30fa8c 100644 --- a/client/src/app/+about/about-instance/about-instance.component.ts +++ b/client/src/app/+about/about-instance/about-instance.component.ts @@ -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() + } + } diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.html b/client/src/app/+about/about-instance/contact-admin-modal.component.html new file mode 100644 index 000000000..b2cbd0873 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.html @@ -0,0 +1,50 @@ + + + + + diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.scss b/client/src/app/+about/about-instance/contact-admin-modal.component.scss new file mode 100644 index 000000000..260d77888 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.scss @@ -0,0 +1,11 @@ +@import 'variables'; +@import 'mixins'; + +input[type=text] { + @include peertube-input-text(340px); + display: block; +} + +textarea { + @include peertube-textarea(100%, 200px); +} diff --git a/client/src/app/+about/about-instance/contact-admin-modal.component.ts b/client/src/app/+about/about-instance/contact-admin-modal.component.ts new file mode 100644 index 000000000..7d79c2215 --- /dev/null +++ b/client/src/app/+about/about-instance/contact-admin-modal.component.ts @@ -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 + } + ) + } +} diff --git a/client/src/app/+about/about-peertube/about-peertube.component.html b/client/src/app/+about/about-peertube/about-peertube.component.html index 13ce89f75..d3fc9a828 100644 --- a/client/src/app/+about/about-peertube/about-peertube.component.html +++ b/client/src/app/+about/about-peertube/about-peertube.component.html @@ -83,7 +83,7 @@
What will be done to mitigate this problem?

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

@@ -94,4 +94,4 @@
  • Disable P2P from the administration interface
  • An automatic video redundancy program: we wouldn't know if the IP downloaded the video on purpose or if it was the automatized program
  • -
    \ No newline at end of file +
    diff --git a/client/src/app/+about/about.module.ts b/client/src/app/+about/about.module.ts index ff6e8ef41..9c6b29740 100644 --- a/client/src/app/+about/about.module.ts +++ b/client/src/app/+about/about.module.ts @@ -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: [ diff --git a/client/src/app/+accounts/account-about/account-about.component.ts b/client/src/app/+accounts/account-about/account-about.component.ts index 6f3e6caa0..13890a0ee 100644 --- a/client/src/app/+accounts/account-about/account-about.component.ts +++ b/client/src/app/+accounts/account-about/account-about.component.ts @@ -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', diff --git a/client/src/app/+accounts/account-videos/account-videos.component.ts b/client/src/app/+accounts/account-videos/account-videos.component.ts index e5c1f58b0..13b634a01 100644 --- a/client/src/app/+accounts/account-videos/account-videos.component.ts +++ b/client/src/app/+accounts/account-videos/account-videos.component.ts @@ -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, diff --git a/client/src/app/+accounts/accounts.component.html b/client/src/app/+accounts/accounts.component.html index 036e794d2..c1377c1ea 100644 --- a/client/src/app/+accounts/accounts.component.html +++ b/client/src/app/+accounts/accounts.component.html @@ -10,8 +10,15 @@
    {{ account.nameWithHost }}
    Banned + Muted + Muted by your instance + Instance muted + Instance muted by your instance - +
    {{ account.followersCount }} subscribers
    diff --git a/client/src/app/+accounts/accounts.component.ts b/client/src/app/+accounts/accounts.component.ts index e19927d6b..e8339b78b 100644 --- a/client/src/app/+accounts/accounts.component.ts +++ b/client/src/app/+accounts/accounts.component.ts @@ -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) ) } } diff --git a/client/src/app/+admin/admin.module.ts b/client/src/app/+admin/admin.module.ts index 8c6db98d9..f7f347105 100644 --- a/client/src/app/+admin/admin.module.ts +++ b/client/src/app/+admin/admin.module.ts @@ -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, diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index e2cbd35ca..52eb00d93 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -7,161 +7,169 @@
    Instance
    -
    - - -
    - {{ formErrors.instanceName }} + +
    + + +
    {{ formErrors.instance.name }}
    -
    -
    - - -
    - {{ formErrors.instanceShortDescription }} +
    + + +
    {{ formErrors.instance.shortDescription }}
    -
    -
    - - -
    - {{ formErrors.instanceDescription }} +
    + + +
    {{ formErrors.instance.description }}
    -
    -
    - - -
    - {{ formErrors.instanceTerms }} +
    + + +
    {{ formErrors.instance.terms }}
    -
    -
    - -
    - +
    + +
    + +
    +
    {{ formErrors.instance.defaultClientRoute }}
    -
    - {{ formErrors.instanceDefaultClientRoute }} -
    -
    -
    - - +
    + + -
    - +
    + +
    +
    {{ formErrors.instance.defaultNSFWPolicy }}
    -
    - {{ formErrors.instanceDefaultNSFWPolicy }} -
    -
    +
    Signup
    - - - - -
    - - -
    - {{ formErrors.signupLimit }} + +
    +
    -
    -
    Import
    - - - - - -
    Administrator
    - -
    - - -
    - {{ formErrors.adminEmail }} +
    +
    -
    + +
    + + +
    {{ formErrors.signup.limit }}
    +
    +
    Users
    -
    - -
    - + +
    + +
    + +
    +
    {{ formErrors.user.videoQuota }}
    -
    - {{ formErrors.userVideoQuota }} + +
    + +
    + +
    +
    {{ formErrors.user.videoQuotaDaily }}
    + + +
    Import
    + + + + +
    + +
    + +
    + +
    + +
    +
    + +
    Administrator
    + +
    + + +
    {{ formErrors.admin.email }}
    -
    - -
    - -
    -
    - {{ formErrors.userVideoQuotaDaily }} -
    +
    +
    + @@ -169,28 +177,35 @@
    Twitter
    -
    - - - -
    - {{ formErrors.servicesTwitterUsername }} -
    -
    + + + +
    + + + +
    {{ formErrors.services.twitter.username }}
    +
    + +
    + +
    + +
    +
    -
    @@ -199,36 +214,48 @@
    Transcoding
    - - - - +
    - -
    - -
    -
    - {{ formErrors.transcodingThreads }} -
    -
    - -
    -
    -
    + + + +
    + +
    + +
    + +
    + +
    +
    {{ formErrors.transcoding.threads }}
    +
    + + +
    + +
    +
    + +
    +
    Cache @@ -239,74 +266,73 @@ >
    -
    - - -
    - {{ formErrors.cachePreviewsSize }} + +
    + + +
    {{ formErrors.cache.previews.size }}
    -
    -
    - - -
    - {{ formErrors.cacheCaptionsSize }} +
    + + +
    {{ formErrors.cache.captions.size }}
    -
    +
    Customizations
    -
    - - - -
    - {{ formErrors.customizationJavascript }} -
    -
    + + +
    + + + +
    {{ formErrors.instance.customizations.javascript }}
    +
    -
    - - + + + +
    {{ formErrors.instance.customizations.css }}
    +
    +
    +
    - Prepend with #custom-css to override styles. Example: -
    -      #custom-css .logged-in-email {{ '{' }}
    -        color: red;
    -      {{ '}' }}
    -                
    - " - > - -
    - {{ formErrors.customizationCSS }} -
    -
    diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index 4983b0425..654a076b0 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -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) } } diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.html b/client/src/app/+admin/follows/followers-list/followers-list.component.html index 5645a60cc..fc022bdb4 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.html +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.html @@ -2,6 +2,15 @@ [value]="followers" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" > + +
    + +
    +
    + ID diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.scss b/client/src/app/+admin/follows/followers-list/followers-list.component.scss index e69de29bb..a6f0656b8 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.scss +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.scss @@ -0,0 +1,10 @@ +@import '_variables'; +@import '_mixins'; + +.caption { + justify-content: flex-end; + + input { + @include peertube-input-text(250px); + } +} \ No newline at end of file diff --git a/client/src/app/+admin/follows/followers-list/followers-list.component.ts b/client/src/app/+admin/follows/followers-list/followers-list.component.ts index ca993dcd3..9a8848bfb 100644 --- a/client/src/app/+admin/follows/followers-list/followers-list.component.ts +++ b/client/src/app/+admin/follows/followers-list/followers-list.component.ts @@ -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) ) } } diff --git a/client/src/app/+admin/follows/following-add/following-add.component.ts b/client/src/app/+admin/follows/following-add/following-add.component.ts index bd9cc022b..2bb249746 100644 --- a/client/src/app/+admin/follows/following-add/following-add.component.ts +++ b/client/src/app/+admin/follows/following-add/following-add.component.ts @@ -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) ) } diff --git a/client/src/app/+admin/follows/following-list/following-list.component.html b/client/src/app/+admin/follows/following-list/following-list.component.html index 8af624ac5..5bc8fbc2d 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.html +++ b/client/src/app/+admin/follows/following-list/following-list.component.html @@ -2,6 +2,17 @@ [value]="following" [lazy]="true" [paginator]="true" [totalRecords]="totalRecords" [rows]="rowsPerPage" [sortField]="sort.field" [sortOrder]="sort.order" (onLazyLoad)="loadLazy($event)" > + +
    +
    + +
    +
    +
    + ID diff --git a/client/src/app/+admin/follows/following-list/following-list.component.scss b/client/src/app/+admin/follows/following-list/following-list.component.scss index bfcdcaa49..a6f0656b8 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.scss +++ b/client/src/app/+admin/follows/following-list/following-list.component.scss @@ -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); } } \ No newline at end of file diff --git a/client/src/app/+admin/follows/following-list/following-list.component.ts b/client/src/app/+admin/follows/following-list/following-list.component.ts index dd57884c6..4517a721e 100644 --- a/client/src/app/+admin/follows/following-list/following-list.component.ts +++ b/client/src/app/+admin/follows/following-list/following-list.component.ts @@ -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) ) } } diff --git a/client/src/app/+admin/follows/shared/follow.service.ts b/client/src/app/+admin/follows/shared/follow.service.ts index 27169a9cd..a2904179e 100644 --- a/client/src/app/+admin/follows/shared/follow.service.ts +++ b/client/src/app/+admin/follows/shared/follow.service.ts @@ -18,10 +18,12 @@ export class FollowService { ) { } - getFollowing (pagination: RestPagination, sort: SortMeta): Observable> { + getFollowing (pagination: RestPagination, sort: SortMeta, search?: string): Observable> { let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) + if (search) params = params.append('search', search) + return this.authHttp.get>(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> { + getFollowers (pagination: RestPagination, sort: SortMeta, search?: string): Observable> { let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) + if (search) params = params.append('search', search) + return this.authHttp.get>(FollowService.BASE_APPLICATION_URL + '/followers', { params }) .pipe( map(res => this.restExtractor.convertResultListDateToHuman(res)), diff --git a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts index 6d77a0eb4..fa1da26bf 100644 --- a/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts +++ b/client/src/app/+admin/follows/shared/redundancy-checkbox.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { I18n } from '@ngx-translate/i18n-polyfill' import { RedundancyService } from '@app/+admin/follows/shared/redundancy.service' @@ -13,24 +13,21 @@ export class RedundancyCheckboxComponent { @Input() host: string constructor ( - private notificationsService: NotificationsService, + private notifier: Notifier, private redundancyService: RedundancyService, private i18n: I18n ) { } updateRedundancyState () { this.redundancyService.updateRedundancy(this.host, this.redundancyAllowed) - .subscribe( - () => { - const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') + .subscribe( + () => { + const stateLabel = this.redundancyAllowed ? this.i18n('enabled') : this.i18n('disabled') - this.notificationsService.success( - this.i18n('Success'), - this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel }) - ) - }, + this.notifier.success(this.i18n('Redundancy for {{host}} is {{stateLabel}}', { host: this.host, stateLabel })) + }, - err => this.notificationsService.error(this.i18n('Error'), err.message) - ) + err => this.notifier.error(err.message) + ) } } diff --git a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts index 866ba1b23..b265e1dd6 100644 --- a/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts +++ b/client/src/app/+admin/jobs/jobs-list/jobs-list.component.ts @@ -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) ) } diff --git a/client/src/app/+admin/moderation/instance-blocklist/index.ts b/client/src/app/+admin/moderation/instance-blocklist/index.ts new file mode 100644 index 000000000..3e7a344bb --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/index.ts @@ -0,0 +1,2 @@ +export * from './instance-account-blocklist.component' +export * from './instance-server-blocklist.component' diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html new file mode 100644 index 000000000..7797bc56e --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.html @@ -0,0 +1,22 @@ + + + + + Account + Muted at + + + + + + {{ accountBlock.blockedAccount.nameWithHost }} + {{ accountBlock.createdAt }} + + + + + + diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts new file mode 100644 index 000000000..032bf745a --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-account-blocklist.component.ts @@ -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) + ) + } +} diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html new file mode 100644 index 000000000..f634ba834 --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.html @@ -0,0 +1,23 @@ + + + + + Instance + Muted at + + + + + + + {{ serverBlock.blockedServer.host }} + {{ serverBlock.createdAt }} + + + + + + diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts new file mode 100644 index 000000000..db3dfcd1c --- /dev/null +++ b/client/src/app/+admin/moderation/instance-blocklist/instance-server-blocklist.component.ts @@ -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) + ) + } +} diff --git a/client/src/app/+admin/moderation/moderation.component.html b/client/src/app/+admin/moderation/moderation.component.html index 91e87fcd4..01457936c 100644 --- a/client/src/app/+admin/moderation/moderation.component.html +++ b/client/src/app/+admin/moderation/moderation.component.html @@ -5,6 +5,10 @@ Video abuses Blacklisted videos + + Muted accounts + + Muted servers
    diff --git a/client/src/app/+admin/moderation/moderation.component.scss b/client/src/app/+admin/moderation/moderation.component.scss index 02ccfc8ca..13b019c5b 100644 --- a/client/src/app/+admin/moderation/moderation.component.scss +++ b/client/src/app/+admin/moderation/moderation.component.scss @@ -10,6 +10,7 @@ font-weight: $font-semibold; min-width: 200px; display: inline-block; + vertical-align: top; } .moderation-expanded-text { diff --git a/client/src/app/+admin/moderation/moderation.component.ts b/client/src/app/+admin/moderation/moderation.component.ts index 0f4efb970..2b2618933 100644 --- a/client/src/app/+admin/moderation/moderation.component.ts +++ b/client/src/app/+admin/moderation/moderation.component.ts @@ -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) + } } diff --git a/client/src/app/+admin/moderation/moderation.routes.ts b/client/src/app/+admin/moderation/moderation.routes.ts index 6d81b9b36..bc6dd49d5 100644 --- a/client/src/app/+admin/moderation/moderation.routes.ts +++ b/client/src/app/+admin/moderation/moderation.routes.ts @@ -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' + } + } } ] } diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html index 3a8424f68..303a788d2 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.html @@ -1,7 +1,8 @@
    -
    +
    This comment can only be seen by you or the other moderators.
    - Cancel + Cancel
    - \ No newline at end of file + diff --git a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts index 34ab384d1..f915978ee 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/moderation-comment-modal.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared' import { I18n } from '@ngx-translate/i18n-polyfill' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' @@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI constructor ( protected formValidatorService: FormValidatorService, private modalService: NgbModal, - private notificationsService: NotificationsService, + private notifier: Notifier, private videoAbuseService: VideoAbuseService, private videoAbuseValidatorsService: VideoAbuseValidatorsService, private i18n: I18n @@ -45,29 +45,26 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI }) } - hideModerationCommentModal () { + hide () { this.abuseToComment = undefined this.openedModal.close() this.form.reset() } async banUser () { - const moderationComment: string = this.form.value['moderationComment'] + const moderationComment: string = this.form.value[ 'moderationComment' ] this.videoAbuseService.updateVideoAbuse(this.abuseToComment, { moderationComment }) - .subscribe( - () => { - this.notificationsService.success( - this.i18n('Success'), - this.i18n('Comment updated.') - ) + .subscribe( + () => { + this.notifier.success(this.i18n('Comment updated.')) - this.commentUpdated.emit(moderationComment) - this.hideModerationCommentModal() - }, + this.commentUpdated.emit(moderationComment) + this.hide() + }, - err => this.notificationsService.error(this.i18n('Error'), err.message) - ) + err => this.notifier.error(err.message) + ) } } diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html index 287ab3e46..05b549de6 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.html @@ -9,7 +9,7 @@ Created Video State - + @@ -41,7 +41,7 @@ - + @@ -51,15 +51,15 @@
    Reason: - {{ videoAbuse.reason }} +
    Moderation comment: - {{ videoAbuse.moderationComment }} +
    - \ No newline at end of file + diff --git a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts index 681db7434..00c871659 100644 --- a/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts +++ b/client/src/app/+admin/moderation/video-abuse-list/video-abuse-list.component.ts @@ -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[] = [] 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) ) } } diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html index 0585e0490..247f441c1 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.html @@ -7,8 +7,9 @@ Video name Sensitive + Unfederated Date - + @@ -26,20 +27,21 @@ - {{ videoBlacklist.video.nsfw }} + {{ booleanToText(videoBlacklist.video.nsfw) }} + {{ booleanToText(videoBlacklist.unfederated) }} {{ videoBlacklist.createdAt }} - + - + Blacklist reason: - {{ videoBlacklist.reason }} + diff --git a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts index bb051d00f..b27bbbfef 100644 --- a/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts +++ b/client/src/app/+admin/moderation/video-blacklist-list/video-blacklist-list.component.ts @@ -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[] = [] 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) ) } } diff --git a/client/src/app/+admin/users/user-edit/index.ts b/client/src/app/+admin/users/user-edit/index.ts index fd80a02e0..ec734ef92 100644 --- a/client/src/app/+admin/users/user-edit/index.ts +++ b/client/src/app/+admin/users/user-edit/index.ts @@ -1,2 +1,3 @@ export * from './user-create.component' export * from './user-update.component' +export * from './user-password.component' diff --git a/client/src/app/+admin/users/user-edit/user-create.component.ts b/client/src/app/+admin/users/user-edit/user-create.component.ts index dd8e4efd5..137ecfcbd 100644 --- a/client/src/app/+admin/users/user-edit/user-create.component.ts +++ b/client/src/app/+admin/users/user-edit/user-create.component.ts @@ -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' ]) }, diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.html b/client/src/app/+admin/users/user-edit/user-edit.component.html index 56cf7d17d..c6566da24 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.html +++ b/client/src/app/+admin/users/user-edit/user-edit.component.html @@ -81,3 +81,17 @@ + +
    + + +
    + + +
    + +
    + + +
    +
    diff --git a/client/src/app/+admin/users/user-edit/user-edit.component.scss b/client/src/app/+admin/users/user-edit/user-edit.component.scss index 6675f65cc..c1cc4ca45 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.component.scss +++ b/client/src/app/+admin/users/user-edit/user-edit.component.scss @@ -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; + } + } +} diff --git a/client/src/app/+admin/users/user-edit/user-edit.ts b/client/src/app/+admin/users/user-edit/user-edit.ts index 07b087b5b..649b35b0c 100644 --- a/client/src/app/+admin/users/user-edit/user-edit.ts +++ b/client/src/app/+admin/users/user-edit/user-edit.ts @@ -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 diff --git a/client/src/app/+admin/users/user-edit/user-password.component.html b/client/src/app/+admin/users/user-edit/user-password.component.html new file mode 100644 index 000000000..a1e1f6216 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.html @@ -0,0 +1,21 @@ +
    +
    + +
    + +
    + +
    +
    +
    + {{ formErrors.password }} +
    +
    + + +
    diff --git a/client/src/app/+admin/users/user-edit/user-password.component.scss b/client/src/app/+admin/users/user-edit/user-password.component.scss new file mode 100644 index 000000000..217d585af --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.scss @@ -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; +} diff --git a/client/src/app/+admin/users/user-edit/user-password.component.ts b/client/src/app/+admin/users/user-edit/user-password.component.ts new file mode 100644 index 000000000..5b3040440 --- /dev/null +++ b/client/src/app/+admin/users/user-edit/user-password.component.ts @@ -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') + } +} diff --git a/client/src/app/+admin/users/user-edit/user-update.component.ts b/client/src/app/+admin/users/user-edit/user-update.component.ts index cd3885a99..94ef87b08 100644 --- a/client/src/app/+admin/users/user-edit/user-update.component.ts +++ b/client/src/app/+admin/users/user-edit/user-update.component.ts @@ -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, diff --git a/client/src/app/+admin/users/user-list/user-list.component.html b/client/src/app/+admin/users/user-list/user-list.component.html index cca057ba1..69a4616a3 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.html +++ b/client/src/app/+admin/users/user-list/user-list.component.html @@ -2,7 +2,7 @@
    Users list
    - + Create user
    @@ -10,9 +10,32 @@ + +
    +
    + + +
    + +
    + +
    +
    +
    + + + + Username Email @@ -25,22 +48,42 @@ - + + + + + + - {{ user.username }} - (banned) + + {{ user.username }} + (banned) + - {{ user.email }} + + {{ user.email }} + + + + ? {{ user.email }} + + + + ✓ {{ user.email }} + + + + {{ user.videoQuotaUsed }} / {{ user.videoQuota }} {{ user.roleLabel }} {{ user.createdAt }} - + @@ -56,3 +99,4 @@
    + diff --git a/client/src/app/+admin/users/user-list/user-list.component.scss b/client/src/app/+admin/users/user-list/user-list.component.scss index 47291918d..5274be01c 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.scss +++ b/client/src/app/+admin/users/user-list/user-list.component.scss @@ -2,7 +2,7 @@ @import '_mixins'; .add-button { - @include create-button('../../../../assets/images/global/add.svg'); + @include create-button; } tr.banned { @@ -15,4 +15,12 @@ tr.banned { .ban-reason-label { font-weight: $font-semibold; -} \ No newline at end of file +} + +.caption { + justify-content: space-between; + + input { + @include peertube-input-text(250px); + } +} diff --git a/client/src/app/+admin/users/user-list/user-list.component.ts b/client/src/app/+admin/users/user-list/user-list.component.ts index dee3ed643..66ab796f9 100644 --- a/client/src/app/+admin/users/user-list/user-list.component.ts +++ b/client/src/app/+admin/users/user-list/user-list.component.ts @@ -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[] = [] + constructor ( - private notificationsService: NotificationsService, + private notifier: Notifier, private confirmService: ConfirmService, + private serverService: ServerService, private userService: UserService, private i18n: I18n ) { super() } + get requiresEmailVerification () { + return this.serverService.getConfig().signup.requiresEmailVerification + } + ngOnInit () { - this.loadSort() + this.initialize() + + this.bulkUserActions = [ + { + label: this.i18n('Delete'), + handler: users => this.removeUsers(users) + }, + { + label: this.i18n('Ban'), + handler: users => this.openBanUserModal(users), + isDisplayed: users => users.every(u => u.blocked === false) + }, + { + label: this.i18n('Unban'), + handler: users => this.unbanUsers(users), + isDisplayed: users => users.every(u => u.blocked === true) + }, + { + label: this.i18n('Set Email as Verified'), + handler: users => this.setEmailsAsVerified(users), + isDisplayed: users => this.requiresEmailVerification && users.every(u => !u.blocked && u.emailVerified === false) + } + ] + } + + openBanUserModal (users: User[]) { + for (const user of users) { + if (user.username === 'root') { + this.notifier.error(this.i18n('You cannot ban root.')) + return + } + } + + this.userBanModal.openModal(users) } onUserChanged () { this.loadData() } - protected loadData () { - this.userService.getUsers(this.pagination, this.sort) - .subscribe( - resultList => { - this.users = resultList.data - this.totalRecords = resultList.total - }, + async unbanUsers (users: User[]) { + const message = this.i18n('Do you really want to unban {{num}} users?', { num: users.length }) - err => this.notificationsService.error(this.i18n('Error'), err.message) - ) + const res = await this.confirmService.confirm(message, this.i18n('Unban')) + if (res === false) return + + this.userService.unbanUsers(users) + .subscribe( + () => { + const message = this.i18n('{{num}} users unbanned.', { num: users.length }) + + this.notifier.success(message) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + } + + async removeUsers (users: User[]) { + for (const user of users) { + if (user.username === 'root') { + this.notifier.error(this.i18n('You cannot delete root.')) + return + } + } + + const message = this.i18n('If you remove these users, you will not be able to create others with the same username!') + const res = await this.confirmService.confirm(message, this.i18n('Delete')) + if (res === false) return + + this.userService.removeUser(users).subscribe( + () => { + this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length })) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + } + + async setEmailsAsVerified (users: User[]) { + this.userService.updateUsers(users, { emailVerified: true }).subscribe( + () => { + this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length })) + this.loadData() + }, + + err => this.notifier.error(err.message) + ) + } + + isInSelectionMode () { + return this.selectedUsers.length !== 0 + } + + protected loadData () { + this.selectedUsers = [] + + this.userService.getUsers(this.pagination, this.sort, this.search) + .subscribe( + resultList => { + this.users = resultList.data + this.totalRecords = resultList.total + }, + + err => this.notifier.error(err.message) + ) } } diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html new file mode 100644 index 000000000..a96a11f5e --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.html @@ -0,0 +1,26 @@ +
    +
    Muted accounts
    +
    + + + + + + Account + Muted at + + + + + + {{ accountBlock.blockedAccount.nameWithHost }} + {{ accountBlock.createdAt }} + + + + + + diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts new file mode 100644 index 000000000..e3025dec4 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-blocklist.component.ts @@ -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) + ) + } +} diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html new file mode 100644 index 000000000..329cfb08f --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.html @@ -0,0 +1,27 @@ +
    +
    Muted instances
    +
    + + + + + + Instance + Muted at + + + + + + + {{ serverBlock.blockedServer.host }} + {{ serverBlock.createdAt }} + + + + + + diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss new file mode 100644 index 000000000..6028b75ea --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.scss @@ -0,0 +1,7 @@ +@import '_variables'; +@import '_mixins'; + +.unblock-button { + @include peertube-button; + @include grey-button; +} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts new file mode 100644 index 000000000..4c5cc28b8 --- /dev/null +++ b/client/src/app/+my-account/my-account-blocklist/my-account-server-blocklist.component.ts @@ -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) + ) + } +} diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html new file mode 100644 index 000000000..d42af37d4 --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html @@ -0,0 +1,27 @@ +
    +
    + + +
    + +
    + +
    +
    + + +
    You don't have videos history yet.
    + +
    +
    +
    + + +
    + {{ video.name }} + {{ video.views | myNumberFormatter }} views + +
    +
    +
    +
    diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss new file mode 100644 index 000000000..e7c6863f1 --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss @@ -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; + } + } +} diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts new file mode 100644 index 000000000..394091bad --- /dev/null +++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts @@ -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) + ) + } +} diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html new file mode 100644 index 000000000..d518b22ec --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.html @@ -0,0 +1,13 @@ +
    + + + Notification preferences + + + +
    + + diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss new file mode 100644 index 000000000..43d1f82ab --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.scss @@ -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; +} diff --git a/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts new file mode 100644 index 000000000..3e197088d --- /dev/null +++ b/client/src/app/+my-account/my-account-notifications/my-account-notifications.component.ts @@ -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() + } +} diff --git a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html index fd7d7d23b..674a4e8a2 100644 --- a/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html +++ b/client/src/app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component.html @@ -1,7 +1,8 @@ - - - - + + + + + @@ -16,4 +17,4 @@ - \ No newline at end of file + diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts index 62053d97b..f4b954e54 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts @@ -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) ) } } diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html index 96629940f..049119fa8 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html +++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.html @@ -15,10 +15,19 @@
    - +
    + +
    + +
    + +
    diff --git a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts index 7089b2057..b8f80bc1a 100644 --- a/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts +++ b/client/src/app/+my-account/my-account-settings/my-account-video-settings/my-account-video-settings.component.ts @@ -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) ) } } diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts index 9517a3705..9d2dccdf0 100644 --- a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts +++ b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts @@ -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) ) } diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts index 81608d837..a68f79b47 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-create.component.ts @@ -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' ]) diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts index ccdd9a3dc..4dc65dd99 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.ts @@ -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 */ } } diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts index 56697030b..da4fb645a 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts @@ -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) ) } diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html index df74b19b6..51db2e75d 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.html @@ -1,6 +1,6 @@ diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss index 472cbb723..77fce138b 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss @@ -2,7 +2,7 @@ @import '_mixins'; .create-button { - @include create-button('../../../assets/images/global/add.svg'); + @include create-button; } /deep/ .action-button { diff --git a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts index 6d1098865..da2c5bcd3 100644 --- a/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts +++ b/client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.ts @@ -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) ) } diff --git a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts index d9fb20446..21a10c8ff 100644 --- a/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts +++ b/client/src/app/+my-account/my-account-video-imports/my-account-video-imports.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core' import { RestPagination, RestTable } from '@app/shared' import { SortMeta } from 'primeng/components/common/sortmeta' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { I18n } from '@ngx-translate/i18n-polyfill' import { VideoImport, VideoImportState } from '../../../../../shared/models/videos' import { VideoImportService } from '@app/shared/video-import' @@ -19,7 +19,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit pagination: RestPagination = { count: this.rowsPerPage, start: 0 } constructor ( - private notificationsService: NotificationsService, + private notifier: Notifier, private videoImportService: VideoImportService, private i18n: I18n ) { @@ -27,7 +27,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit } ngOnInit () { - this.loadSort() + this.initialize() } isVideoImportSuccess (videoImport: VideoImport) { @@ -58,7 +58,7 @@ export class MyAccountVideoImportsComponent extends RestTable implements OnInit this.totalRecords = resultList.total }, - err => this.notificationsService.error(this.i18n('Error'), err.message) + err => this.notifier.error(err.message) ) } } diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html index a6911e4bf..69748ef37 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html @@ -32,7 +32,7 @@ - + Delete
    @@ -45,7 +45,7 @@
    @@ -53,4 +53,4 @@
    - \ No newline at end of file + diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss index 2db81a3fe..39d0cf2f7 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss @@ -23,14 +23,11 @@ .action-button-delete-selection { @include peertube-button; @include orange-button; - } + @include button-with-icon(21px); - .icon.icon-delete-white { - @include icon(21px); - - position: relative; - top: -2px; - background-image: url('../../../assets/images/global/delete-white.svg'); + my-global-icon { + @include apply-svg-color(#fff); + } } } } @@ -97,7 +94,7 @@ } } -@media screen and (max-width: 800px) { +@media screen and (max-width: $small-view) { .video { flex-direction: column; height: auto; diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts index 7560f0128..41608f796 100644 --- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts +++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.ts @@ -5,7 +5,7 @@ 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 { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { AuthService } from '../../core/auth' import { ConfirmService } from '../../core/confirm' import { AbstractVideoList } from '../../shared/video/abstract-video-list' @@ -40,7 +40,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni protected router: Router, protected route: ActivatedRoute, protected authService: AuthService, - protected notificationsService: NotificationsService, + protected notifier: Notifier, protected location: Location, protected screenService: ScreenService, protected i18n: I18n, @@ -102,16 +102,13 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni .pipe(concatAll()) .subscribe( res => { - this.notificationsService.success( - this.i18n('Success'), - this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length }) - ) + this.notifier.success(this.i18n('{{deleteLength}} videos deleted.', { deleteLength: toDeleteVideosIds.length })) this.abortSelectionMode() this.reloadVideos() }, - err => this.notificationsService.error(this.i18n('Error'), err.message) + err => this.notifier.error(err.message) ) } @@ -124,15 +121,12 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni this.videoService.removeVideo(video.id) .subscribe( - status => { - this.notificationsService.success( - this.i18n('Success'), - this.i18n('Video {{videoName}} deleted.', { videoName: video.name }) - ) + () => { + this.notifier.success(this.i18n('Video {{videoName}} deleted.', { videoName: video.name })) this.reloadVideos() }, - error => this.notificationsService.error(this.i18n('Error'), error.message) + error => this.notifier.error(error.message) ) } @@ -169,7 +163,7 @@ export class MyAccountVideosComponent extends AbstractVideoList implements OnIni private spliceVideosById (id: number) { for (const key of Object.keys(this.loadedPages)) { - const videos = this.loadedPages[ key ] + const videos: Video[] = this.loadedPages[ key ] const index = videos.findIndex(v => v.id === id) if (index !== -1) { diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html index 69b198faa..22f127904 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.html @@ -1,7 +1,8 @@
    diff --git a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts index 7437b939a..37d7cf2a4 100644 --- a/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts +++ b/client/src/app/+my-account/my-account-videos/video-change-ownership/video-change-ownership.component.ts @@ -1,5 +1,5 @@ import { Component, ElementRef, OnInit, ViewChild } from '@angular/core' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { NgbModal } from '@ng-bootstrap/ng-bootstrap' import { FormReactive, UserService } from '../../../shared/index' import { Video } from '@app/shared/video/video.model' @@ -25,7 +25,7 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni protected formValidatorService: FormValidatorService, private videoChangeOwnershipValidatorsService: VideoChangeOwnershipValidatorsService, private videoOwnershipService: VideoOwnershipService, - private notificationsService: NotificationsService, + private notifier: Notifier, private userService: UserService, private modalService: NgbModal, private i18n: I18n @@ -49,15 +49,13 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni .catch((_) => _) // Called when closing (cancel) the modal without validating, do nothing } - search (event) { + search (event: { query: string }) { const query = event.query this.userService.autocomplete(query) .subscribe( - usernames => { - this.usernamePropositions = usernames - }, + usernames => this.usernamePropositions = usernames, - err => this.notificationsService.error('Error', err.message) + err => this.notifier.error(err.message) ) } @@ -67,9 +65,9 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni this.videoOwnershipService .changeOwnership(this.video.id, username) .subscribe( - () => this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership change request sent.')), + () => this.notifier.success(this.i18n('Ownership change request sent.')), - err => this.notificationsService.error(this.i18n('Error'), err.message) + err => this.notifier.error(err.message) ) } } diff --git a/client/src/app/+my-account/my-account.component.html b/client/src/app/+my-account/my-account.component.html index b602fd69f..3999252be 100644 --- a/client/src/app/+my-account/my-account.component.html +++ b/client/src/app/+my-account/my-account.component.html @@ -1,26 +1,5 @@
    - +
    diff --git a/client/src/app/+my-account/my-account.component.scss b/client/src/app/+my-account/my-account.component.scss index 20b2639b5..4f111efdf 100644 --- a/client/src/app/+my-account/my-account.component.scss +++ b/client/src/app/+my-account/my-account.component.scss @@ -1,14 +1,3 @@ -.my-library { - span[role=button] { - cursor: pointer; - } - - a { - display: block; - } +.row { + flex-direction: column; } - -/deep/ .dropdown-toggle::after { - position: relative; - top: 2px; -} \ No newline at end of file diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts index bad60a8fb..8a4102d80 100644 --- a/client/src/app/+my-account/my-account.component.ts +++ b/client/src/app/+my-account/my-account.component.ts @@ -1,37 +1,80 @@ -import { Component, OnDestroy, OnInit } from '@angular/core' +import { Component } from '@angular/core' import { ServerService } from '@app/core' -import { NavigationStart, Router } from '@angular/router' -import { filter } from 'rxjs/operators' import { I18n } from '@ngx-translate/i18n-polyfill' -import { Subscription } from 'rxjs' +import { TopMenuDropdownParam } from '@app/shared/menu/top-menu-dropdown.component' @Component({ selector: 'my-my-account', templateUrl: './my-account.component.html', styleUrls: [ './my-account.component.scss' ] }) -export class MyAccountComponent implements OnInit, OnDestroy { - - libraryLabel = '' - - private routeSub: Subscription +export class MyAccountComponent { + menuEntries: TopMenuDropdownParam[] = [] constructor ( private serverService: ServerService, - private router: Router, private i18n: I18n - ) {} + ) { - ngOnInit () { - this.updateLibraryLabel(this.router.url) + const libraryEntries: TopMenuDropdownParam = { + label: this.i18n('My library'), + children: [ + { + label: this.i18n('My channels'), + routerLink: '/my-account/video-channels' + }, + { + label: this.i18n('My videos'), + routerLink: '/my-account/videos' + }, + { + label: this.i18n('My subscriptions'), + routerLink: '/my-account/subscriptions' + }, + { + label: this.i18n('My history'), + routerLink: '/my-account/history/videos' + } + ] + } - this.routeSub = this.router.events - .pipe(filter(event => event instanceof NavigationStart)) - .subscribe((event: NavigationStart) => this.updateLibraryLabel(event.url)) - } + if (this.isVideoImportEnabled()) { + libraryEntries.children.push({ + label: 'My imports', + routerLink: '/my-account/video-imports' + }) + } - ngOnDestroy () { - if (this.routeSub) this.routeSub.unsubscribe() + const miscEntries: TopMenuDropdownParam = { + label: this.i18n('Misc'), + children: [ + { + label: this.i18n('Muted accounts'), + routerLink: '/my-account/blocklist/accounts' + }, + { + label: this.i18n('Muted instances'), + routerLink: '/my-account/blocklist/servers' + }, + { + label: this.i18n('Ownership changes'), + routerLink: '/my-account/ownership' + } + ] + } + + this.menuEntries = [ + { + label: this.i18n('My settings'), + routerLink: '/my-account/settings' + }, + { + label: this.i18n('My notifications'), + routerLink: '/my-account/notifications' + }, + libraryEntries, + miscEntries + ] } isVideoImportEnabled () { @@ -40,19 +83,4 @@ export class MyAccountComponent implements OnInit, OnDestroy { return importConfig.http.enabled || importConfig.torrent.enabled } - private updateLibraryLabel (url: string) { - const [ path ] = url.split('?') - - if (path.startsWith('/my-account/video-channels')) { - this.libraryLabel = this.i18n('Channels') - } else if (path.startsWith('/my-account/videos')) { - this.libraryLabel = this.i18n('Videos') - } else if (path.startsWith('/my-account/subscriptions')) { - this.libraryLabel = this.i18n('Subscriptions') - } else if (path.startsWith('/my-account/video-imports')) { - this.libraryLabel = this.i18n('Video imports') - } else { - this.libraryLabel = '' - } - } } diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts index ad21162a8..18f51f171 100644 --- a/client/src/app/+my-account/my-account.module.ts +++ b/client/src/app/+my-account/my-account.module.ts @@ -1,6 +1,7 @@ import { TableModule } from 'primeng/table' import { NgModule } from '@angular/core' import { AutoCompleteModule } from 'primeng/autocomplete' +import { InputSwitchModule } from 'primeng/inputswitch' import { SharedModule } from '../shared' import { MyAccountRoutingModule } from './my-account-routing.module' import { MyAccountChangePasswordComponent } from './my-account-settings/my-account-change-password/my-account-change-password.component' @@ -19,6 +20,11 @@ import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-i import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component' import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone' import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.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' +import { MyAccountNotificationPreferencesComponent } from '@app/+my-account/my-account-settings/my-account-notification-preferences' @NgModule({ imports: [ @@ -26,7 +32,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub MyAccountRoutingModule, AutoCompleteModule, SharedModule, - TableModule + TableModule, + InputSwitchModule ], declarations: [ @@ -45,7 +52,12 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub ActorAvatarInfoComponent, MyAccountVideoImportsComponent, MyAccountDangerZoneComponent, - MyAccountSubscriptionsComponent + MyAccountSubscriptionsComponent, + MyAccountBlocklistComponent, + MyAccountServerBlocklistComponent, + MyAccountHistoryComponent, + MyAccountNotificationsComponent, + MyAccountNotificationPreferencesComponent ], exports: [ diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.ts b/client/src/app/+my-account/shared/actor-avatar-info.component.ts index 7b80b1ed4..72c815a0c 100644 --- a/client/src/app/+my-account/shared/actor-avatar-info.component.ts +++ b/client/src/app/+my-account/shared/actor-avatar-info.component.ts @@ -1,8 +1,8 @@ -import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core' import { ServerService } from '../../core/server' -import { NotificationsService } from 'angular2-notifications' import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { Account } from '@app/shared/account/account.model' +import { Notifier } from '@app/core' @Component({ selector: 'my-actor-avatar-info', @@ -10,7 +10,7 @@ import { Account } from '@app/shared/account/account.model' styleUrls: [ './actor-avatar-info.component.scss' ] }) export class ActorAvatarInfoComponent { - @ViewChild('avatarfileInput') avatarfileInput + @ViewChild('avatarfileInput') avatarfileInput: ElementRef @Input() actor: VideoChannel | Account @@ -18,13 +18,13 @@ export class ActorAvatarInfoComponent { constructor ( private serverService: ServerService, - private notificationsService: NotificationsService + private notifier: Notifier ) {} onAvatarChange () { const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] if (avatarfile.size > this.maxAvatarSize) { - this.notificationsService.error('Error', 'This image is too large.') + this.notifier.error('Error', 'This image is too large.') return } diff --git a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts index 995f42ffc..cfd471fa4 100644 --- a/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts +++ b/client/src/app/+verify-account/verify-account-ask-send-email/verify-account-ask-send-email.component.ts @@ -1,9 +1,8 @@ import { Component, OnInit } from '@angular/core' import { I18n } from '@ngx-translate/i18n-polyfill' -import { NotificationsService } from 'angular2-notifications' +import { Notifier, RedirectService } from '@app/core' import { ServerService } from '@app/core/server' -import { RedirectService } from '@app/core' -import { UserService, FormReactive } from '@app/shared' +import { FormReactive, UserService } from '@app/shared' import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service' import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service' @@ -20,7 +19,7 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements private userValidatorsService: UserValidatorsService, private userService: UserService, private serverService: ServerService, - private notificationsService: NotificationsService, + private notifier: Notifier, private redirectService: RedirectService, private i18n: I18n ) { @@ -46,12 +45,12 @@ export class VerifyAccountAskSendEmailComponent extends FormReactive implements 'An email with verification link will be sent to {{email}}.', { email } ) - this.notificationsService.success(this.i18n('Success'), message) + this.notifier.success(message) this.redirectService.redirectToHomepage() }, err => { - this.notificationsService.error(this.i18n('Error'), err.message) + this.notifier.error(err.message) } ) } diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html index 30ace5e10..a83d4a3c2 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.html @@ -9,7 +9,7 @@
    diff --git a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts index 26b3bf4b1..f9ecf664b 100644 --- a/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts +++ b/client/src/app/+verify-account/verify-account-email/verify-account-email.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core' import { ActivatedRoute, Router } from '@angular/router' import { I18n } from '@ngx-translate/i18n-polyfill' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core' import { UserService } from '@app/shared' @Component({ @@ -17,7 +17,7 @@ export class VerifyAccountEmailComponent implements OnInit { constructor ( private userService: UserService, - private notificationsService: NotificationsService, + private notifier: Notifier, private router: Router, private route: ActivatedRoute, private i18n: I18n @@ -25,12 +25,11 @@ export class VerifyAccountEmailComponent implements OnInit { } ngOnInit () { - this.userId = this.route.snapshot.queryParams['userId'] this.verificationString = this.route.snapshot.queryParams['verificationString'] if (!this.userId || !this.verificationString) { - this.notificationsService.error(this.i18n('Error'), this.i18n('Unable to find user id or verification string.')) + this.notifier.error(this.i18n('Unable to find user id or verification string.')) } else { this.verifyEmail() } @@ -47,7 +46,7 @@ export class VerifyAccountEmailComponent implements OnInit { }, err => { - this.notificationsService.error(this.i18n('Error'), err.message) + this.notifier.error(err.message) } ) } diff --git a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts index ea7b0e118..895b19064 100644 --- a/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts +++ b/client/src/app/+video-channels/video-channel-about/video-channel-about.component.ts @@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser import { VideoChannel } from '@app/shared/video-channel/video-channel.model' 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-video-channel-about', diff --git a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts index 800d97b7f..dea378a6e 100644 --- a/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts +++ b/client/src/app/+video-channels/video-channel-videos/video-channel-videos.component.ts @@ -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-video-channel-videos', @@ -25,7 +25,7 @@ import { ScreenService } from '@app/shared/misc/screen.service' export class VideoChannelVideosComponent extends AbstractVideoList implements OnInit, OnDestroy { titlePage: string marginContent = false // Disable margin - currentRoute = '/video-channel/videos' + currentRoute = '/video-channels/videos' loadOnInit = false private videoChannel: VideoChannel @@ -35,7 +35,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On protected router: Router, protected route: ActivatedRoute, protected authService: AuthService, - protected notificationsService: NotificationsService, + protected notifier: Notifier, protected confirmService: ConfirmService, protected location: Location, protected screenService: ScreenService, @@ -55,7 +55,7 @@ export class VideoChannelVideosComponent extends AbstractVideoList implements On this.videoChannelSub = this.videoChannelService.videoChannelLoaded .subscribe(videoChannel => { this.videoChannel = videoChannel - this.currentRoute = '/video-channel/' + this.videoChannel.uuid + '/videos' + this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos' this.reloadVideos() this.generateSyndicationList() diff --git a/client/src/app/+video-channels/video-channels-routing.module.ts b/client/src/app/+video-channels/video-channels-routing.module.ts index 935578d2a..3ac3533d9 100644 --- a/client/src/app/+video-channels/video-channels-routing.module.ts +++ b/client/src/app/+video-channels/video-channels-routing.module.ts @@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel- const videoChannelsRoutes: Routes = [ { - path: ':videoChannelId', + path: ':videoChannelName', component: VideoChannelsComponent, canActivateChild: [ MetaGuard ], children: [ diff --git a/client/src/app/+video-channels/video-channels.component.ts b/client/src/app/+video-channels/video-channels.component.ts index 0c5c814c7..41ff82e98 100644 --- a/client/src/app/+video-channels/video-channels.component.ts +++ b/client/src/app/+video-channels/video-channels.component.ts @@ -34,9 +34,9 @@ export class VideoChannelsComponent implements OnInit, OnDestroy { ngOnInit () { this.routeSub = this.route.params .pipe( - map(params => params[ 'videoChannelId' ]), + map(params => params[ 'videoChannelName' ]), distinctUntilChanged(), - switchMap(videoChannelId => this.videoChannelService.getVideoChannel(videoChannelId)), + switchMap(videoChannelName => this.videoChannelService.getVideoChannel(videoChannelName)), catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) ) .subscribe(videoChannel => this.videoChannel = videoChannel) diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index 545d6aeda..cff37a7d6 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -43,7 +43,8 @@ const routes: Routes = [ imports: [ RouterModule.forRoot(routes, { useHash: Boolean(history.pushState) === false, - preloadingStrategy: PreloadSelectedModulesList + preloadingStrategy: PreloadSelectedModulesList, + anchorScrolling: 'enabled' }) ], providers: [ diff --git a/client/src/app/app.component.html b/client/src/app/app.component.html index 03f7e88ed..d398d4f35 100644 --- a/client/src/app/app.component.html +++ b/client/src/app/app.component.html @@ -30,12 +30,27 @@
    + - + + + +
    +
    +

    {{ message.summary }}

    +

    {{ message.detail }}

    +
    + + + + +
    +
    +
    diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index b51a81eb1..881f3ff31 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -91,8 +91,3 @@ footer { height: $footer-height; justify-content: center; } - -simple-notifications { - position: relative; - z-index: 1500; -} diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index dc4d0bf6a..7583fdee8 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -15,19 +15,6 @@ import { fromEvent } from 'rxjs' styleUrls: [ './app.component.scss' ] }) export class AppComponent implements OnInit { - notificationOptions = { - timeOut: 5000, - lastOnBottom: true, - clickToClose: true, - maxLength: 0, - maxStack: 7, - showProgressBar: false, - pauseOnHover: false, - preventDuplicates: false, - preventLastDuplicates: 'visible', - rtl: false - } - isMenuDisplayed = true isMenuChangedByUser = false diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts index 34e890b40..0bbc2e08b 100644 --- a/client/src/app/app.module.ts +++ b/client/src/app/app.module.ts @@ -12,13 +12,12 @@ import { AppComponent } from './app.component' import { CoreModule } from './core' import { HeaderComponent } from './header' import { LoginModule } from './login' -import { MenuComponent } from './menu' +import { AvatarNotificationComponent, LanguageChooserComponent, MenuComponent } from './menu' import { SharedModule } from './shared' import { SignupModule } from './signup' import { VideosModule } from './videos' import { buildFileLocale, getCompleteLocale, isDefaultLocale } from '../../../shared/models/i18n' import { getDevLocale, isOnDevLocale } from '@app/shared/i18n/i18n-utils' -import { LanguageChooserComponent } from '@app/menu/language-chooser.component' import { SearchModule } from '@app/search' export function metaFactory (serverService: ServerService): MetaLoader { @@ -40,6 +39,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { MenuComponent, LanguageChooserComponent, + AvatarNotificationComponent, HeaderComponent ], imports: [ @@ -69,7 +69,7 @@ export function metaFactory (serverService: ServerService): MetaLoader { providers: [ { provide: TRANSLATIONS, - useFactory: (locale) => { + useFactory: (locale: string) => { // On dev mode, test localization if (isOnDevLocale()) { locale = buildFileLocale(getDevLocale()) diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 74ed1c580..abb11fdc2 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -1,8 +1,9 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' import { UserRight } from '../../../../../shared/models/users/user-right.enum' +import { User as ServerUserModel } from '../../../../../shared/models/users/user.model' // Do not use the barrel (dependency loop) import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' -import { User, UserConstructorHash } from '../../shared/users/user.model' +import { User } from '../../shared/users/user.model' import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' export type TokenOptions = { @@ -70,8 +71,10 @@ export class AuthUser extends User { ID: 'id', ROLE: 'role', EMAIL: 'email', + VIDEOS_HISTORY_ENABLED: 'videos-history-enabled', USERNAME: 'username', NSFW_POLICY: 'nsfw_policy', + WEBTORRENT_ENABLED: 'peertube-videojs-' + 'webtorrent_enabled', AUTO_PLAY_VIDEO: 'auto_play_video' } @@ -87,7 +90,9 @@ export class AuthUser extends User { email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, - autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' + webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true', + autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true', + videosHistoryEnabled: peertubeLocalStorage.getItem(this.KEYS.VIDEOS_HISTORY_ENABLED) === 'true' }, Tokens.load() ) @@ -101,12 +106,14 @@ export class AuthUser extends User { peertubeLocalStorage.removeItem(this.KEYS.ID) peertubeLocalStorage.removeItem(this.KEYS.ROLE) peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) + peertubeLocalStorage.removeItem(this.KEYS.WEBTORRENT_ENABLED) + peertubeLocalStorage.removeItem(this.KEYS.VIDEOS_HISTORY_ENABLED) peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) peertubeLocalStorage.removeItem(this.KEYS.EMAIL) Tokens.flush() } - constructor (userHash: UserConstructorHash, hashTokens: TokenOptions) { + constructor (userHash: Partial, hashTokens: TokenOptions) { super(userHash) this.tokens = new Tokens(hashTokens) } @@ -138,6 +145,7 @@ export class AuthUser extends User { peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) + peertubeLocalStorage.setItem(AuthUser.KEYS.WEBTORRENT_ENABLED, JSON.stringify(this.webTorrentEnabled)) peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) this.tokens.save() } diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9c36b946e..eaa822e0f 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -3,18 +3,18 @@ import { catchError, map, mergeMap, share, tap } from 'rxjs/operators' import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import { Injectable } from '@angular/core' import { Router } from '@angular/router' -import { NotificationsService } from 'angular2-notifications' +import { Notifier } from '@app/core/notification/notifier.service' import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared' import { User } from '../../../../../shared/models/users' import { UserLogin } from '../../../../../shared/models/users/user-login.model' import { environment } from '../../../environments/environment' -import { RestExtractor } from '../../shared/rest' +import { RestExtractor } from '../../shared/rest/rest-extractor.service' import { AuthStatus } from './auth-status.model' import { AuthUser } from './auth-user.model' import { objectToUrlEncoded } from '@app/shared/misc/utils' import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' import { I18n } from '@ngx-translate/i18n-polyfill' -import { HotkeysService, Hotkey } from 'angular2-hotkeys' +import { Hotkey, HotkeysService } from 'angular2-hotkeys' interface UserLoginWithUsername extends UserLogin { access_token: string @@ -38,7 +38,6 @@ export class AuthService { loginChangedSource: Observable userInformationLoaded = new ReplaySubject(1) hotkeys: Hotkey[] - redirectUrl: string private clientId: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_ID) private clientSecret: string = peertubeLocalStorage.getItem(AuthService.LOCAL_STORAGE_OAUTH_CLIENT_KEYS.CLIENT_SECRET) @@ -48,7 +47,7 @@ export class AuthService { constructor ( private http: HttpClient, - private notificationsService: NotificationsService, + private notifier: Notifier, private hotkeysService: HotkeysService, private restExtractor: RestExtractor, private router: Router, @@ -106,9 +105,8 @@ export class AuthService { ) } - // We put a bigger timeout - // This is an important message - this.notificationsService.error(this.i18n('Error'), errorMessage, { timeOut: 7000 }) + // We put a bigger timeout: this is an important message + this.notifier.error(errorMessage, this.i18n('Error'), 7000) } ) } @@ -178,8 +176,6 @@ export class AuthService { this.setStatus(AuthStatus.LoggedOut) this.hotkeysService.remove(this.hotkeys) - - this.redirectUrl = null } refreshAccessToken () { @@ -221,7 +217,7 @@ export class AuthService { } refreshUserInformation () { - const obj = { + const obj: UserLoginWithUsername = { access_token: this.user.getAccessToken(), refresh_token: null, token_type: this.user.getTokenType(), diff --git a/client/src/app/core/auth/index.ts b/client/src/app/core/auth/index.ts index bc7bfec0e..8e5caa7ed 100644 --- a/client/src/app/core/auth/index.ts +++ b/client/src/app/core/auth/index.ts @@ -1,4 +1,3 @@ export * from './auth-status.model' export * from './auth-user.model' export * from './auth.service' -export * from '../routing/login-guard.service' diff --git a/client/src/app/core/confirm/index.ts b/client/src/app/core/confirm/index.ts index 44aabfc13..aca591e1a 100644 --- a/client/src/app/core/confirm/index.ts +++ b/client/src/app/core/confirm/index.ts @@ -1,2 +1 @@ -export * from './confirm.component' export * from './confirm.service' diff --git a/client/src/app/core/core.module.ts b/client/src/app/core/core.module.ts index df2ec696d..4ef3b1e73 100644 --- a/client/src/app/core/core.module.ts +++ b/client/src/app/core/core.module.ts @@ -7,16 +7,18 @@ import { LoadingBarModule } from '@ngx-loading-bar/core' import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client' import { LoadingBarRouterModule } from '@ngx-loading-bar/router' -import { SimpleNotificationsModule } from 'angular2-notifications' - import { AuthService } from './auth' -import { ConfirmComponent, ConfirmService } from './confirm' +import { ConfirmService } from './confirm' import { throwIfAlreadyLoaded } from './module-import-guard' import { LoginGuard, RedirectService, UserRightGuard } from './routing' import { ServerService } from './server' import { ThemeService } from './theme' import { HotkeyModule } from 'angular2-hotkeys' -import { CheatSheetComponent } from '@app/core/hotkeys' +import { CheatSheetComponent } from './hotkeys' +import { ToastModule } from 'primeng/toast' +import { Notifier } from './notification' +import { MessageService } from 'primeng/api' +import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service' @NgModule({ imports: [ @@ -25,11 +27,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys' FormsModule, BrowserAnimationsModule, - SimpleNotificationsModule.forRoot(), - LoadingBarHttpClientModule, LoadingBarRouterModule, - LoadingBarModule.forRoot(), + LoadingBarModule, + ToastModule, HotkeyModule.forRoot({ cheatSheetCloseEsc: true @@ -37,16 +38,15 @@ import { CheatSheetComponent } from '@app/core/hotkeys' ], declarations: [ - ConfirmComponent, CheatSheetComponent ], exports: [ - SimpleNotificationsModule, LoadingBarHttpClientModule, LoadingBarModule, - ConfirmComponent, + ToastModule, + CheatSheetComponent ], @@ -57,7 +57,10 @@ import { CheatSheetComponent } from '@app/core/hotkeys' ThemeService, LoginGuard, UserRightGuard, - RedirectService + RedirectService, + Notifier, + MessageService, + UserNotificationSocket ] }) export class CoreModule { diff --git a/client/src/app/core/index.ts b/client/src/app/core/index.ts index 524589d74..f664aff41 100644 --- a/client/src/app/core/index.ts +++ b/client/src/app/core/index.ts @@ -2,6 +2,7 @@ export * from './auth' export * from './confirm' export * from './routing' export * from './server' +export * from './notification' export * from './theme' export * from './core.module' diff --git a/client/src/app/core/notification/index.ts b/client/src/app/core/notification/index.ts new file mode 100644 index 000000000..3e8d9ea65 --- /dev/null +++ b/client/src/app/core/notification/index.ts @@ -0,0 +1,2 @@ +export * from './notifier.service' +export * from './user-notification-socket.service' diff --git a/client/src/app/core/notification/notifier.service.ts b/client/src/app/core/notification/notifier.service.ts new file mode 100644 index 000000000..9833c65a0 --- /dev/null +++ b/client/src/app/core/notification/notifier.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core' +import { MessageService } from 'primeng/api' +import { I18n } from '@ngx-translate/i18n-polyfill' + +@Injectable() +export class Notifier { + readonly TIMEOUT = 5000 + + constructor ( + private i18n: I18n, + private messageService: MessageService) { + } + + info (text: string, title?: string, timeout?: number) { + if (!title) title = this.i18n('Info') + + return this.notify('info', text, title, timeout) + } + + error (text: string, title?: string, timeout?: number) { + if (!title) title = this.i18n('Error') + + return this.notify('error', text, title, timeout) + } + + success (text: string, title?: string, timeout?: number) { + if (!title) title = this.i18n('Success') + + return this.notify('success', text, title, timeout) + } + + private notify (severity: 'success' | 'info' | 'warn' | 'error', text: string, title: string, timeout?: number) { + this.messageService.add({ + severity, + summary: title, + detail: text, + closable: true, + life: timeout || this.TIMEOUT + }) + } +} diff --git a/client/src/app/core/notification/user-notification-socket.service.ts b/client/src/app/core/notification/user-notification-socket.service.ts new file mode 100644 index 000000000..f367d9ae4 --- /dev/null +++ b/client/src/app/core/notification/user-notification-socket.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core' +import { environment } from '../../../environments/environment' +import { UserNotification as UserNotificationServer } from '../../../../../shared' +import { Subject } from 'rxjs' +import * as io from 'socket.io-client' +import { AuthService } from '../auth' + +export type NotificationEvent = 'new' | 'read' | 'read-all' + +@Injectable() +export class UserNotificationSocket { + private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>() + + private socket: SocketIOClient.Socket + + constructor ( + private auth: AuthService + ) {} + + dispatch (type: NotificationEvent, notification?: UserNotificationServer) { + this.notificationSubject.next({ type, notification }) + } + + getMyNotificationsSocket () { + const socket = this.getSocket() + + socket.on('new-notification', (n: UserNotificationServer) => this.dispatch('new', n)) + + return this.notificationSubject.asObservable() + } + + private getSocket () { + if (this.socket) return this.socket + + this.socket = io(environment.apiUrl + '/user-notifications', { + query: { accessToken: this.auth.getAccessToken() } + }) + + return this.socket + } +} diff --git a/client/src/app/core/routing/login-guard.service.ts b/client/src/app/core/routing/login-guard.service.ts index 40ff8f505..7b1c37ee8 100644 --- a/client/src/app/core/routing/login-guard.service.ts +++ b/client/src/app/core/routing/login-guard.service.ts @@ -1,11 +1,5 @@ import { Injectable } from '@angular/core' -import { - ActivatedRouteSnapshot, - CanActivateChild, - RouterStateSnapshot, - CanActivate, - Router -} from '@angular/router' +import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router' import { AuthService } from '../auth/auth.service' @@ -20,8 +14,6 @@ export class LoginGuard implements CanActivate, CanActivateChild { canActivate (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (this.auth.isLoggedIn() === true) return true - this.auth.redirectUrl = state.url - this.router.navigate([ '/login' ]) return false } diff --git a/client/src/app/core/routing/redirect.service.ts b/client/src/app/core/routing/redirect.service.ts index 1881be117..e1db4097b 100644 --- a/client/src/app/core/routing/redirect.service.ts +++ b/client/src/app/core/routing/redirect.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@angular/core' -import { Router } from '@angular/router' +import { NavigationEnd, Router } from '@angular/router' import { ServerService } from '../server' @Injectable() @@ -8,6 +8,9 @@ export class RedirectService { static INIT_DEFAULT_ROUTE = '/videos/trending' static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE + private previousUrl: string + private currentUrl: string + constructor ( private router: Router, private serverService: ServerService @@ -18,6 +21,7 @@ export class RedirectService { RedirectService.DEFAULT_ROUTE = config.instance.defaultClientRoute } + // Load default route this.serverService.configLoaded .subscribe(() => { const defaultRouteConfig = this.serverService.getConfig().instance.defaultClientRoute @@ -26,6 +30,21 @@ export class RedirectService { RedirectService.DEFAULT_ROUTE = defaultRouteConfig } }) + + // Track previous url + this.currentUrl = this.router.url + router.events.subscribe(event => { + if (event instanceof NavigationEnd) { + this.previousUrl = this.currentUrl + this.currentUrl = event.url + } + }) + } + + redirectToPreviousRoute () { + if (this.previousUrl) return this.router.navigateByUrl(this.previousUrl) + + return this.redirectToHomepage() } redirectToHomepage (skipLocationChange = false) { diff --git a/client/src/app/core/routing/user-right-guard.service.ts b/client/src/app/core/routing/user-right-guard.service.ts index 65d029977..50c3d8c19 100644 --- a/client/src/app/core/routing/user-right-guard.service.ts +++ b/client/src/app/core/routing/user-right-guard.service.ts @@ -7,7 +7,7 @@ import { Router } from '@angular/router' -import { AuthService } from '../auth' +import { AuthService } from '../auth/auth.service' @Injectable() export class UserRightGuard implements CanActivate, CanActivateChild { diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 2f1ef1fc2..c868ccdcc 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -13,6 +13,7 @@ import { sortBy } from '@app/shared/misc/utils' @Injectable() export class ServerService { + private static BASE_SERVER_URL = environment.apiUrl + '/api/v1/server/' private static BASE_CONFIG_URL = environment.apiUrl + '/api/v1/config/' private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/' @@ -37,6 +38,12 @@ export class ServerService { css: '' } }, + email: { + enabled: false + }, + contactForm: { + enabled: false + }, serverVersion: 'Unknown', signup: { allowed: false, @@ -44,7 +51,10 @@ export class ServerService { requiresEmailVerification: false }, transcoding: { - enabledResolutions: [] + enabledResolutions: [], + hls: { + enabled: false + } }, avatar: { file: { @@ -80,6 +90,11 @@ export class ServerService { enabled: false } } + }, + trending: { + videos: { + intervalDays: 0 + } } } private videoCategories: Array> = [] @@ -141,10 +156,6 @@ export class ServerService { return this.videoPrivacies } - getAbout () { - return this.http.get(ServerService.BASE_CONFIG_URL + '/about') - } - private loadVideoAttributeEnum ( attributeName: 'categories' | 'licences' | 'languages' | 'privacies', hashToPopulate: VideoConstant[], @@ -154,7 +165,7 @@ export class ServerService { this.localeObservable .pipe( switchMap(translations => { - return this.http.get(ServerService.BASE_VIDEO_URL + attributeName) + return this.http.get<{ [id: string]: string }>(ServerService.BASE_VIDEO_URL + attributeName) .pipe(map(data => ({ data, translations }))) }) ) diff --git a/client/src/app/core/theme/theme.service.ts b/client/src/app/core/theme/theme.service.ts index a6eef0898..50c19ecac 100644 --- a/client/src/app/core/theme/theme.service.ts +++ b/client/src/app/core/theme/theme.service.ts @@ -5,7 +5,7 @@ import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage' export class ThemeService { private theme = document.querySelector('body') private darkTheme = false - private previousTheme = {} + private previousTheme: { [ id: string ]: string } = {} constructor () { // initialise the alternative theme with dark theme colors @@ -33,7 +33,7 @@ export class ThemeService { } } - private switchProperty (property, newValue?) { + private switchProperty (property: string, newValue?: string) { const propertyOldvalue = window.getComputedStyle(this.theme).getPropertyValue('--' + property) this.theme.style.setProperty('--' + property, (newValue) ? newValue : this.previousTheme[property]) this.previousTheme[property] = propertyOldvalue diff --git a/client/src/app/header/header.component.html b/client/src/app/header/header.component.html index a04354db5..46a87c79c 100644 --- a/client/src/app/header/header.component.html +++ b/client/src/app/header/header.component.html @@ -1,10 +1,10 @@ - + Upload diff --git a/client/src/app/header/header.component.scss b/client/src/app/header/header.component.scss index bd03c338a..cea415d9b 100644 --- a/client/src/app/header/header.component.scss +++ b/client/src/app/header/header.component.scss @@ -6,6 +6,7 @@ padding-left: 10px; margin-right: 15px; padding-right: 40px; // For the search icon + font-size: 14px; &::placeholder { color: var(--inputPlaceholderColor); @@ -40,6 +41,7 @@ .upload-button { @include peertube-button-link; @include orange-button; + @include button-with-icon(22px, 3px, -1px); margin-right: 25px; @@ -47,15 +49,6 @@ margin-right: 0; } - .icon.icon-upload { - @include icon(22px); - - background-image: url('../../assets/images/header/upload.svg'); - height: 24px; - vertical-align: middle; - margin-right: 6px; - } - @media screen and (max-width: 600px) { margin-right: 10px; padding: 0 10px; diff --git a/client/src/app/login/login.component.html b/client/src/app/login/login.component.html index 93dbed525..4efe3fb22 100644 --- a/client/src/app/login/login.component.html +++ b/client/src/app/login/login.component.html @@ -55,11 +55,17 @@