Merge branch 'release/v1.2.0'

pull/1830/head
Chocobozzz 2019-02-06 12:26:58 +01:00
commit 73471b1a52
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
631 changed files with 26005 additions and 12579 deletions

View File

@ -48,12 +48,12 @@ matrix:
- env: TEST_SUITE=jest
script:
- travis_retry npm run travis -- "$TEST_SUITE"
- NODE_PENDING_JOB_WAIT=1000 travis_retry npm run travis -- "$TEST_SUITE"
after_failure:
- cat test1/logs/all-logs.log
- cat test2/logs/all-logs.log
- cat test3/logs/all-logs.log
- cat test4/logs/all-logs.log
- cat test5/logs/all-logs.log
- cat test6/logs/all-logs.log
- cat test1/logs/peertube.log
- cat test2/logs/peertube.log
- cat test3/logs/peertube.log
- cat test4/logs/peertube.log
- cat test5/logs/peertube.log
- cat test6/logs/peertube.log

View File

@ -1,9 +1,101 @@
# 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))
@ -26,7 +118,6 @@
* 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

View File

@ -6,25 +6,27 @@
* [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)
* [joshmorel](https://github.com/joshmorel)
* [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)
* [thomaskuntzz](https://github.com/thomaskuntzz)
* [rezonant](https://github.com/rezonant)
* [ldidry](https://github.com/ldidry)
* [McFlat](https://github.com/McFlat)
* [okhin](https://github.com/okhin)
* [daftaupe](https://github.com/daftaupe)
* [thomaskuntzz](https://github.com/thomaskuntzz)
* [LecygneNoir](https://github.com/LecygneNoir)
* [fflorent](https://github.com/fflorent)
* [dedesite](https://github.com/dedesite)
* [Nautigsam](https://github.com/Nautigsam)
* [scanlime](https://github.com/scanlime)
* [tcitworld](https://github.com/tcitworld)
* [am97](https://github.com/am97)
* [dadall](https://github.com/dadall)
@ -35,7 +37,6 @@
* [jocelynj](https://github.com/jocelynj)
* [lucas-dclrcq](https://github.com/lucas-dclrcq)
* [lucaspontoexe](https://github.com/lucaspontoexe)
* [scanlime](https://github.com/scanlime)
* [flyingrub](https://github.com/flyingrub)
* [SerCom-KC](https://github.com/SerCom-KC)
* [valvin1](https://github.com/valvin1)
@ -43,6 +44,7 @@
* [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)
@ -65,6 +67,7 @@
* [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)
@ -73,7 +76,9 @@
* [zapashcanon](https://github.com/zapashcanon)
* [mart-e](https://github.com/mart-e)
* [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)
@ -125,11 +130,14 @@
* [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)
@ -167,6 +175,10 @@
* [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

14
FAQ.md
View File

@ -13,6 +13,8 @@
- [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)
@ -89,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:

View File

@ -16,6 +16,16 @@
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.
</p>
<p align="center">
<strong>Developed with &#10084; by <a href="https://framasoft.org">Framasoft</a></strong>
</p>
<p align="center">
<a href="https://framasoft.org">
<img width="150px" src="http://lutim.cpy.re/Prd3ci7G.png" alt="Framasoft logo"/>
</a>
</p>
<p align="center">
<strong>Client</strong>
@ -123,7 +133,7 @@ You can also join the cheerful bunch that makes our community:
* Chat<a name="contact"></a>:
* 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)**
* 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)

View File

@ -1,6 +1,6 @@
{
"name": "peertube-client",
"version": "1.1.0",
"version": "1.2.0",
"private": true,
"licence": "GPLv3",
"author": {
@ -63,26 +63,26 @@
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.10.0",
"@angular/animations": "~7.0.2",
"@angular/cli": "~7.0.4",
"@angular/common": "~7.0.2",
"@angular/compiler": "~7.0.2",
"@angular/compiler-cli": "~7.0.2",
"@angular/core": "~7.0.2",
"@angular/forms": "~7.0.2",
"@angular/http": "~7.0.2",
"@angular/language-service": "~7.0.2",
"@angular/platform-browser": "~7.0.2",
"@angular/platform-browser-dynamic": "~7.0.2",
"@angular/router": "~7.0.2",
"@angular/service-worker": "~7.0.2",
"@angular-devkit/build-angular": "~0.11.1",
"@angular/animations": "~7.1.1",
"@angular/cli": "~7.1.1",
"@angular/common": "~7.1.1",
"@angular/compiler": "~7.1.1",
"@angular/compiler-cli": "~7.1.1",
"@angular/core": "~7.1.1",
"@angular/forms": "~7.1.1",
"@angular/http": "~7.1.1",
"@angular/language-service": "~7.1.1",
"@angular/platform-browser": "~7.1.1",
"@angular/platform-browser-dynamic": "~7.1.1",
"@angular/router": "~7.1.1",
"@angular/service-worker": "~7.1.1",
"@angularclass/hmr": "^2.1.3",
"@neos21/bootstrap3-glyphicons": "^1.0.1",
"@ng-bootstrap/ng-bootstrap": "^4.0.0",
"@ngx-loading-bar/core": "^2.2.0",
"@ngx-loading-bar/http-client": "^2.2.0",
"@ngx-loading-bar/router": "^2.2.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",
"@types/core-js": "^2.5.0",
@ -94,10 +94,10 @@
"@types/markdown-it": "^0.0.5",
"@types/node": "^10.9.2",
"@types/sanitize-html": "1.18.0",
"@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",
@ -132,7 +132,7 @@
"node-sass": "^4.9.3",
"npm-font-source-sans-pro": "^1.0.2",
"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",
@ -142,6 +142,7 @@
"sanitize-html": "^1.18.4",
"sass-loader": "^7.1.0",
"sass-resources-loader": "^2.0.0",
"socket.io-client": "^2.2.0",
"stream-browserify": "^2.0.1",
"stream-http": "^3.0.0",
"terser-webpack-plugin": "^1.1.0",
@ -153,7 +154,6 @@
"videojs-contextmenu-ui": "^5.0.0",
"videojs-dock": "^2.0.2",
"videojs-hotkeys": "^0.2.21",
"webpack": "^4.17.1",
"webpack-bundle-analyzer": "^3.0.2",
"webpack-cli": "^3.0.8",
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",

View File

@ -1,39 +1,52 @@
<div i18n class="about-instance-title">
About {{ instanceName }} instance
</div>
<div class="row">
<div class="col-md-12 col-xl-6">
<div class="about-instance-title">
<div i18n>About {{ instanceName }} instance</div>
<div class="short-description">
<div>{{ shortDescription }}</div>
</div>
<div *ngIf="isContactFormEnabled" (click)="openContactModal()" i18n role="button" class="contact-admin">Contact administrator</div>
</div>
<div class="description">
<div i18n class="section-title">Description</div>
<div class="short-description">
<div>{{ shortDescription }}</div>
</div>
<div [innerHTML]="descriptionHTML"></div>
</div>
<div class="description">
<div i18n class="section-title">Description</div>
<div class="terms" id="terms-section">
<div i18n class="section-title">Terms</div>
<div [innerHTML]="descriptionHTML"></div>
</div>
<div [innerHTML]="termsHTML"></div>
</div>
<div class="terms" id="terms-section">
<div i18n class="section-title">Terms</div>
<div class="signup">
<div i18n class="section-title">Signup</div>
<div [innerHTML]="termsHTML"></div>
</div>
<div *ngIf="isSignupAllowed">
<ng-container i18n>User registration is allowed and</ng-container>
<div class="signup">
<div i18n class="section-title">Signup</div>
<ng-container i18n *ngIf="userVideoQuota !== -1">
this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
</ng-container>
<div *ngIf="isSignupAllowed">
<ng-container i18n>User registration is allowed and</ng-container>
<ng-container i18n *ngIf="userVideoQuota === -1">
this instance provides unlimited space for the videos of its users.
</ng-container>
<ng-container i18n *ngIf="userVideoQuota !== -1">
this instance provides a baseline quota of {{ userVideoQuota | bytes: 0 }} space for the videos of its users.
</ng-container>
<ng-container i18n *ngIf="userVideoQuota === -1">
this instance provides unlimited space for the videos of its users.
</ng-container>
</div>
<div i18n *ngIf="isSignupAllowed === false">
User registration is currently not allowed.
</div>
</div>
</div>
<div i18n *ngIf="isSignupAllowed === false">
User registration is currently not allowed.
<div class="col-md-12 col-xl-6">
<label>Features found on this instance</label>
<my-instance-features-table></my-instance-features-table>
</div>
</div>
</div>
<my-contact-admin-modal #contactAdminModal></my-contact-admin-modal>

View File

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

View File

@ -1,23 +1,26 @@
import { Component, OnInit } from '@angular/core'
import { ServerService } from '@app/core'
import { MarkdownService } from '@app/videos/shared'
import { NotificationsService } from 'angular2-notifications'
import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
import { InstanceService } from '@app/shared/instance/instance.service'
import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-about-instance',
templateUrl: './about-instance.component.html',
styleUrls: [ './about-instance.component.scss' ]
})
export class AboutInstanceComponent implements OnInit {
@ViewChild('contactAdminModal') contactAdminModal: ContactAdminModalComponent
shortDescription = ''
descriptionHTML = ''
termsHTML = ''
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private serverService: ServerService,
private instanceService: InstanceService,
private markdownService: MarkdownService,
private i18n: I18n
) {}
@ -34,8 +37,12 @@ export class AboutInstanceComponent implements OnInit {
return this.serverService.getConfig().signup.allowed
}
get isContactFormEnabled () {
return this.serverService.getConfig().email.enabled && this.serverService.getConfig().contactForm.enabled
}
ngOnInit () {
this.serverService.getAbout()
this.instanceService.getAbout()
.subscribe(
res => {
this.shortDescription = res.instance.shortDescription
@ -43,8 +50,12 @@ export class AboutInstanceComponent implements OnInit {
this.termsHTML = this.markdownService.textMarkdownToHTML(res.instance.terms)
},
err => this.notificationsService.error(this.i18n('Error getting about from server'), err)
() => this.notifier.error(this.i18n('Cannot get about information from server'))
)
}
openContactModal () {
return this.contactAdminModal.show()
}
}

View File

@ -0,0 +1,50 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
</div>
<div class="modal-body">
<form novalidate [formGroup]="form" (ngSubmit)="sendForm()">
<div class="form-group">
<label i18n for="fromName">Your name</label>
<input
type="text" id="fromName"
formControlName="fromName" [ngClass]="{ 'input-error': formErrors.fromName }"
>
<div *ngIf="formErrors.fromName" class="form-error">{{ formErrors.fromName }}</div>
</div>
<div class="form-group">
<label i18n for="fromEmail">Your email</label>
<input
type="text" id="fromEmail"
formControlName="fromEmail" [ngClass]="{ 'input-error': formErrors['fromEmail'] }"
>
<div *ngIf="formErrors.fromEmail" class="form-error">{{ formErrors.fromEmail }}</div>
</div>
<div class="form-group">
<label i18n for="body">Your message</label>
<textarea id="body" formControlName="body" [ngClass]="{ 'input-error': formErrors['body'] }">
</textarea>
<div *ngIf="formErrors.body" class="form-error">{{ formErrors.body }}</div>
</div>
<div *ngIf="error" class="alert alert-danger">{{ error }}</div>
<div class="form-group inputs">
<span i18n class="action-button action-button-cancel" (click)="hide()">
Cancel
</span>
<input
type="submit" i18n-value value="Submit" class="action-button-submit"
[disabled]="!form.valid"
>
</div>
</form>
</div>
</ng-template>

View File

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

View File

@ -0,0 +1,77 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
import { FormReactive, InstanceValidatorsService } from '@app/shared'
import { InstanceService } from '@app/shared/instance/instance.service'
@Component({
selector: 'my-contact-admin-modal',
templateUrl: './contact-admin-modal.component.html',
styleUrls: [ './contact-admin-modal.component.scss' ]
})
export class ContactAdminModalComponent extends FormReactive implements OnInit {
@ViewChild('modal') modal: NgbModal
error: string
private openedModal: NgbModalRef
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private instanceValidatorsService: InstanceValidatorsService,
private instanceService: InstanceService,
private serverService: ServerService,
private notifier: Notifier,
private i18n: I18n
) {
super()
}
get instanceName () {
return this.serverService.getConfig().instance.name
}
ngOnInit () {
this.buildForm({
fromName: this.instanceValidatorsService.FROM_NAME,
fromEmail: this.instanceValidatorsService.FROM_EMAIL,
body: this.instanceValidatorsService.BODY
})
}
show () {
this.openedModal = this.modalService.open(this.modal, { keyboard: false })
}
hide () {
this.form.reset()
this.error = undefined
this.openedModal.close()
this.openedModal = null
}
sendForm () {
const fromName = this.form.value['fromName']
const fromEmail = this.form.value[ 'fromEmail' ]
const body = this.form.value[ 'body' ]
this.instanceService.contactAdministrator(fromEmail, fromName, body)
.subscribe(
() => {
this.notifier.success(this.i18n('Your message has been sent.'))
this.hide()
},
err => {
this.error = err.status === 403
? this.i18n('You already sent this form recently')
: err.message
}
)
}
}

View File

@ -5,6 +5,7 @@ import { AboutComponent } from './about.component'
import { SharedModule } from '../shared'
import { AboutInstanceComponent } from '@app/+about/about-instance/about-instance.component'
import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertube.component'
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
@NgModule({
imports: [
@ -15,7 +16,8 @@ import { AboutPeertubeComponent } from '@app/+about/about-peertube/about-peertub
declarations: [
AboutComponent,
AboutInstanceComponent,
AboutPeertubeComponent
AboutPeertubeComponent,
ContactAdminModalComponent
],
exports: [

View File

@ -1,9 +1,9 @@
import { Component, OnInit, OnDestroy } from '@angular/core'
import { Component, OnDestroy, OnInit } from '@angular/core'
import { Account } from '@app/shared/account/account.model'
import { AccountService } from '@app/shared/account/account.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { MarkdownService } from '@app/videos/shared'
import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-account-about',

View File

@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { ScreenService } from '@app/shared/misc/screen.service'
import { Notifier } from '@app/core'
@Component({
selector: 'my-account-videos',
@ -35,7 +35,7 @@ export class AccountVideosComponent extends AbstractVideoList implements OnInit,
protected router: Router,
protected route: ActivatedRoute,
protected authService: AuthService,
protected notificationsService: NotificationsService,
protected notifier: Notifier,
protected confirmService: ConfirmService,
protected location: Location,
protected screenService: ScreenService,

View File

@ -5,10 +5,9 @@ import { Account } from '@app/shared/account/account.model'
import { RestExtractor, UserService } from '@app/shared'
import { catchError, distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'
import { Subscription } from 'rxjs'
import { NotificationsService } from 'angular2-notifications'
import { AuthService, Notifier, RedirectService } from '@app/core'
import { User, UserRight } from '../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { AuthService, RedirectService } from '@app/core'
@Component({
templateUrl: './accounts.component.html',
@ -24,11 +23,10 @@ export class AccountsComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private userService: UserService,
private accountService: AccountService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private restExtractor: RestExtractor,
private redirectService: RedirectService,
private authService: AuthService,
private i18n: I18n
private authService: AuthService
) {}
ngOnInit () {
@ -43,7 +41,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
.subscribe(
account => this.account = account,
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
@ -69,7 +67,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
.subscribe(
user => this.user = user,
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -7,169 +7,169 @@
<div i18n class="inner-form-title">Instance</div>
<div class="form-group">
<label i18n for="instanceName">Name</label>
<input
type="text" id="instanceName"
formControlName="instanceName" [ngClass]="{ 'input-error': formErrors['instanceName'] }"
>
<div *ngIf="formErrors.instanceName" class="form-error">
{{ formErrors.instanceName }}
<ng-container formGroupName="instance">
<div class="form-group">
<label i18n for="instanceName">Name</label>
<input
type="text" id="instanceName"
formControlName="name" [ngClass]="{ 'input-error': formErrors.instance.name }"
>
<div *ngIf="formErrors.instance.name" class="form-error">{{ formErrors.instance.name }}</div>
</div>
</div>
<div class="form-group">
<label i18n for="instanceShortDescription">Short description</label>
<textarea
id="instanceShortDescription" formControlName="instanceShortDescription"
[ngClass]="{ 'input-error': formErrors['instanceShortDescription'] }"
></textarea>
<div *ngIf="formErrors.instanceShortDescription" class="form-error">
{{ formErrors.instanceShortDescription }}
<div class="form-group">
<label i18n for="instanceShortDescription">Short description</label>
<textarea
id="instanceShortDescription" formControlName="shortDescription"
[ngClass]="{ 'input-error': formErrors['instance.shortDescription'] }"
></textarea>
<div *ngIf="formErrors.instance.shortDescription" class="form-error">{{ formErrors.instance.shortDescription }}</div>
</div>
</div>
<div class="form-group">
<label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
<my-markdown-textarea
id="instanceDescription" formControlName="instanceDescription" textareaWidth="500px" [previewColumn]="true"
[classes]="{ 'input-error': formErrors['instanceDescription'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instanceDescription" class="form-error">
{{ formErrors.instanceDescription }}
<div class="form-group">
<label i18n for="instanceDescription">Description</label><my-help helpType="markdownText"></my-help>
<my-markdown-textarea
id="instanceDescription" formControlName="description" textareaWidth="500px" [previewColumn]="true"
[classes]="{ 'input-error': formErrors['instance.description'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instance.description" class="form-error">{{ formErrors.instance.description }}</div>
</div>
</div>
<div class="form-group">
<label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
<my-markdown-textarea
id="instanceTerms" formControlName="instanceTerms" textareaWidth="500px" [previewColumn]="true"
[ngClass]="{ 'input-error': formErrors['instanceTerms'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instanceTerms" class="form-error">
{{ formErrors.instanceTerms }}
<div class="form-group">
<label i18n for="instanceTerms">Terms</label><my-help helpType="markdownText"></my-help>
<my-markdown-textarea
id="instanceTerms" formControlName="terms" textareaWidth="500px" [previewColumn]="true"
[ngClass]="{ 'input-error': formErrors['instance.terms'] }"
></my-markdown-textarea>
<div *ngIf="formErrors.instance.terms" class="form-error">{{ formErrors.instance.terms }}</div>
</div>
</div>
<div class="form-group">
<label i18n for="instanceDefaultClientRoute">Default client route</label>
<div class="peertube-select-container">
<select id="instanceDefaultClientRoute" formControlName="instanceDefaultClientRoute">
<option i18n value="/videos/overview">Videos Overview</option>
<option i18n value="/videos/trending">Videos Trending</option>
<option i18n value="/videos/recently-added">Videos Recently Added</option>
<option i18n value="/videos/local">Local videos</option>
</select>
<div class="form-group">
<label i18n for="instanceDefaultClientRoute">Default client route</label>
<div class="peertube-select-container">
<select id="instanceDefaultClientRoute" formControlName="defaultClientRoute">
<option i18n value="/videos/overview">Videos Overview</option>
<option i18n value="/videos/trending">Videos Trending</option>
<option i18n value="/videos/recently-added">Videos Recently Added</option>
<option i18n value="/videos/local">Local videos</option>
</select>
</div>
<div *ngIf="formErrors.instance.defaultClientRoute" class="form-error">{{ formErrors.instance.defaultClientRoute }}</div>
</div>
<div *ngIf="formErrors.instanceDefaultClientRoute" class="form-error">
{{ formErrors.instanceDefaultClientRoute }}
</div>
</div>
<div class="form-group">
<label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
></my-help>
<div class="form-group">
<label i18n for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."
></my-help>
<div class="peertube-select-container">
<select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
<option i18n value="do_not_list">Do not list</option>
<option i18n value="blur">Blur thumbnails</option>
<option i18n value="display">Display</option>
</select>
<div class="peertube-select-container">
<select id="instanceDefaultNSFWPolicy" formControlName="defaultNSFWPolicy">
<option i18n value="do_not_list">Do not list</option>
<option i18n value="blur">Blur thumbnails</option>
<option i18n value="display">Display</option>
</select>
</div>
<div *ngIf="formErrors.instance.defaultNSFWPolicy" class="form-error">{{ formErrors.instance.defaultNSFWPolicy }}</div>
</div>
<div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
{{ formErrors.instanceDefaultNSFWPolicy }}
</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Signup</div>
<div class="form-group">
<my-peertube-checkbox
inputName="signupEnabled" formControlName="signupEnabled"
i18n-labelText labelText="Signup enabled"
></my-peertube-checkbox>
</div>
<div class="form-group">
<my-peertube-checkbox *ngIf="isSignupEnabled()"
inputName="signupRequiresEmailVerification" formControlName="signupRequiresEmailVerification"
i18n-labelText labelText="Signup requires email verification"
></my-peertube-checkbox>
</div>
<div *ngIf="isSignupEnabled()" class="form-group">
<label i18n for="signupLimit">Signup limit</label>
<input
type="text" id="signupLimit"
formControlName="signupLimit" [ngClass]="{ 'input-error': formErrors['signupLimit'] }"
>
<div *ngIf="formErrors.signupLimit" class="form-error">
{{ formErrors.signupLimit }}
<ng-container formGroupName="signup">
<div class="form-group">
<my-peertube-checkbox
inputName="signupEnabled" formControlName="enabled"
i18n-labelText labelText="Signup enabled"
></my-peertube-checkbox>
</div>
</div>
<div i18n class="inner-form-title">Import</div>
<div class="form-group">
<my-peertube-checkbox
inputName="importVideosHttpEnabled" formControlName="importVideosHttpEnabled"
i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
></my-peertube-checkbox>
</div>
<div class="form-group">
<my-peertube-checkbox
inputName="importVideosTorrentEnabled" formControlName="importVideosTorrentEnabled"
i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
></my-peertube-checkbox>
</div>
<div i18n class="inner-form-title">Administrator</div>
<div class="form-group">
<label i18n for="adminEmail">Admin email</label>
<input
type="text" id="adminEmail"
formControlName="adminEmail" [ngClass]="{ 'input-error': formErrors['adminEmail'] }"
>
<div *ngIf="formErrors.adminEmail" class="form-error">
{{ formErrors.adminEmail }}
<div class="form-group">
<my-peertube-checkbox *ngIf="isSignupEnabled()"
inputName="signupRequiresEmailVerification" formControlName="requiresEmailVerification"
i18n-labelText labelText="Signup requires email verification"
></my-peertube-checkbox>
</div>
</div>
<div *ngIf="isSignupEnabled()" class="form-group">
<label i18n for="signupLimit">Signup limit</label>
<input
type="text" id="signupLimit"
formControlName="limit" [ngClass]="{ 'input-error': formErrors['signup.limit'] }"
>
<div *ngIf="formErrors.signup.limit" class="form-error">{{ formErrors.signup.limit }}</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Users</div>
<div class="form-group">
<label i18n for="userVideoQuota">User default video quota</label>
<div class="peertube-select-container">
<select id="userVideoQuota" formControlName="userVideoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
<ng-container formGroupName="user">
<div class="form-group">
<label i18n for="userVideoQuota">User default video quota</label>
<div class="peertube-select-container">
<select id="userVideoQuota" formControlName="videoQuota">
<option *ngFor="let videoQuotaOption of videoQuotaOptions" [value]="videoQuotaOption.value">
{{ videoQuotaOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.user.videoQuota" class="form-error">{{ formErrors.user.videoQuota }}</div>
</div>
<div *ngIf="formErrors.userVideoQuota" class="form-error">
{{ formErrors.userVideoQuota }}
<div class="form-group">
<label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
<div class="peertube-select-container">
<select id="userVideoQuotaDaily" formControlName="videoQuotaDaily">
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
{{ videoQuotaDailyOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.user.videoQuotaDaily" class="form-error">{{ formErrors.user.videoQuotaDaily }}</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Import</div>
<ng-container formGroupName="import">
<ng-container formGroupName="videos">
<div class="form-group" formGroupName="http">
<my-peertube-checkbox
inputName="importVideosHttpEnabled" formControlName="enabled"
i18n-labelText labelText="Video import with HTTP URL (i.e. YouTube) enabled"
></my-peertube-checkbox>
</div>
<div class="form-group" formGroupName="torrent">
<my-peertube-checkbox
inputName="importVideosTorrentEnabled" formControlName="enabled"
i18n-labelText labelText="Video import with a torrent file or a magnet URI enabled"
></my-peertube-checkbox>
</div>
</ng-container>
</ng-container>
<div i18n class="inner-form-title">Administrator</div>
<div class="form-group" formGroupName="admin">
<label i18n for="adminEmail">Admin email</label>
<input
type="text" id="adminEmail"
formControlName="email" [ngClass]="{ 'input-error': formErrors['admin.email'] }"
>
<div *ngIf="formErrors.admin.email" class="form-error">{{ formErrors.admin.email }}</div>
</div>
<div class="form-group">
<label i18n for="userVideoQuotaDaily">User default daily upload limit</label>
<div class="peertube-select-container">
<select id="userVideoQuotaDaily" formControlName="userVideoQuotaDaily">
<option *ngFor="let videoQuotaDailyOption of videoQuotaDailyOptions" [value]="videoQuotaDailyOption.value">
{{ videoQuotaDailyOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.userVideoQuotaDaily" class="form-error">
{{ formErrors.userVideoQuotaDaily }}
</div>
<div class="form-group" formGroupName="contactForm">
<my-peertube-checkbox
inputName="enableContactForm" formControlName="enabled"
i18n-labelText labelText="Enable contact form"
></my-peertube-checkbox>
</div>
</ng-template>
</ngb-tab>
@ -177,30 +177,35 @@
<ng-template ngbTabContent>
<div i18n class="inner-form-title">Twitter</div>
<div class="form-group">
<label i18n for="signupLimit">Your Twitter username</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="Indicates the Twitter account for the website or platform on which the content was published."
></my-help>
<input
type="text" id="servicesTwitterUsername"
formControlName="servicesTwitterUsername" [ngClass]="{ 'input-error': formErrors['servicesTwitterUsername'] }"
>
<div *ngIf="formErrors.servicesTwitterUsername" class="form-error">
{{ formErrors.servicesTwitterUsername }}
</div>
</div>
<ng-container formGroupName="services">
<ng-container formGroupName="twitter">
<div class="form-group">
<label i18n for="signupLimit">Your Twitter username</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="Indicates the Twitter account for the website or platform on which the content was published."
></my-help>
<input
type="text" id="servicesTwitterUsername"
formControlName="username" [ngClass]="{ 'input-error': formErrors['services.twitter.username'] }"
>
<div *ngIf="formErrors.services.twitter.username" class="form-error">{{ formErrors.services.twitter.username }}</div>
</div>
<div class="form-group">
<my-peertube-checkbox
inputName="servicesTwitterWhitelisted" formControlName="whitelisted"
i18n-labelText labelText="Instance whitelisted by Twitter"
i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
></my-peertube-checkbox>
</div>
</ng-container>
</ng-container>
<div class="form-group">
<my-peertube-checkbox
inputName="servicesTwitterWhitelisted" formControlName="servicesTwitterWhitelisted"
i18n-labelText labelText="Instance whitelisted by Twitter"
i18n-helpHtml helpHtml="If your instance is whitelisted by Twitter, a video player will be embedded in the Twitter feed on PeerTube video share.<br />
If the instance is not whitelisted, we use an image link card that will redirect on your PeerTube instance.<br /><br />
Check this checkbox, save the configuration and test with a video URL of your instance (https://example.com/videos/watch/blabla) on <a target='_blank' rel='noopener noreferrer' href='https://cards-dev.twitter.com/validator'>https://cards-dev.twitter.com/validator</a> to see if you instance is whitelisted."
></my-peertube-checkbox>
</div>
</ng-template>
</ngb-tab>
@ -209,37 +214,48 @@
<div i18n class="inner-form-title">Transcoding</div>
<div class="form-group">
<my-peertube-checkbox
inputName="transcodingEnabled" formControlName="transcodingEnabled"
i18n-labelText labelText="Transcoding enabled"
i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
></my-peertube-checkbox>
</div>
<ng-template [ngIf]="isTranscodingEnabled()">
<ng-container formGroupName="transcoding">
<div class="form-group">
<label i18n for="transcodingThreads">Transcoding threads</label>
<div class="peertube-select-container">
<select id="transcodingThreads" formControlName="transcodingThreads">
<option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
{{ transcodingThreadOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.transcodingThreads" class="form-error">
{{ formErrors.transcodingThreads }}
</div>
</div>
<div class="form-group" *ngFor="let resolution of resolutions">
<my-peertube-checkbox
[inputName]="getResolutionKey(resolution)" [formControlName]="getResolutionKey(resolution)"
i18n-labelText labelText="Resolution {{resolution}} enabled"
inputName="transcodingEnabled" formControlName="enabled"
i18n-labelText labelText="Transcoding enabled"
i18n-helpHtml helpHtml="If you disable transcoding, many videos from your users will not work!"
></my-peertube-checkbox>
</div>
</ng-template>
<ng-container *ngIf="isTranscodingEnabled()">
<div class="form-group">
<my-peertube-checkbox
inputName="transcodingAllowAdditionalExtensions" formControlName="allowAdditionalExtensions"
i18n-labelText labelText="Allow additional extensions"
i18n-helpHtml helpHtml="Allow your users to upload .mkv, .mov, .avi, .flv videos"
></my-peertube-checkbox>
</div>
<div class="form-group">
<label i18n for="transcodingThreads">Transcoding threads</label>
<div class="peertube-select-container">
<select id="transcodingThreads" formControlName="threads">
<option *ngFor="let transcodingThreadOption of transcodingThreadOptions" [value]="transcodingThreadOption.value">
{{ transcodingThreadOption.label }}
</option>
</select>
</div>
<div *ngIf="formErrors.transcoding.threads" class="form-error">{{ formErrors.transcoding.threads }}</div>
</div>
<ng-container formGroupName="resolutions">
<div class="form-group" *ngFor="let resolution of resolutions">
<my-peertube-checkbox
[inputName]="getResolutionKey(resolution)" [formControlName]="resolution"
i18n-labelText labelText="Resolution {{resolution}} enabled"
></my-peertube-checkbox>
</div>
</ng-container>
</ng-container>
</ng-container>
<div i18n class="inner-form-title">
Cache
@ -250,74 +266,73 @@
></my-help>
</div>
<div class="form-group">
<label i18n for="cachePreviewsSize">Previews cache size</label>
<input
type="text" id="cachePreviewsSize"
formControlName="cachePreviewsSize" [ngClass]="{ 'input-error': formErrors['cachePreviewsSize'] }"
>
<div *ngIf="formErrors.cachePreviewsSize" class="form-error">
{{ formErrors.cachePreviewsSize }}
<ng-container formGroupName="cache">
<div class="form-group" formGroupName="previews">
<label i18n for="cachePreviewsSize">Previews cache size</label>
<input
type="text" id="cachePreviewsSize"
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.previews.size'] }"
>
<div *ngIf="formErrors.cache.previews.size" class="form-error">{{ formErrors.cache.previews.size }}</div>
</div>
</div>
<div class="form-group">
<label i18n for="cachePreviewsSize">Video captions cache size</label>
<input
type="text" id="cacheCaptionsSize"
formControlName="cacheCaptionsSize" [ngClass]="{ 'input-error': formErrors['cacheCaptionsSize'] }"
>
<div *ngIf="formErrors.cacheCaptionsSize" class="form-error">
{{ formErrors.cacheCaptionsSize }}
<div class="form-group" formGroupName="captions">
<label i18n for="cacheCaptionsSize">Video captions cache size</label>
<input
type="text" id="cacheCaptionsSize"
formControlName="size" [ngClass]="{ 'input-error': formErrors['cache.captions.size'] }"
>
<div *ngIf="formErrors.cache.captions.size" class="form-error">{{ formErrors.cache.captions.size }}</div>
</div>
</div>
</ng-container>
<div i18n class="inner-form-title">Customizations</div>
<div class="form-group">
<label i18n for="customizationJavascript">JavaScript</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
></my-help>
<textarea
id="customizationJavascript" formControlName="customizationJavascript"
[ngClass]="{ 'input-error': formErrors['customizationJavascript'] }"
></textarea>
<div *ngIf="formErrors.customizationJavascript" class="form-error">
{{ formErrors.customizationJavascript }}
</div>
</div>
<ng-container formGroupName="instance">
<ng-container formGroupName="customizations">
<div class="form-group">
<label i18n for="customizationJavascript">JavaScript</label>
<my-help
helpType="custom" i18n-customHtml
customHtml="Write directly JavaScript code.<br />Example: <pre>console.log('my instance is amazing');</pre>"
></my-help>
<textarea
id="customizationJavascript" formControlName="javascript"
[ngClass]="{ 'input-error': formErrors['instance.customizations.javascript'] }"
></textarea>
<div *ngIf="formErrors.instance.customizations.javascript" class="form-error">{{ formErrors.instance.customizations.javascript }}</div>
</div>
<div class="form-group">
<label for="customizationCSS">CSS</label>
<my-help
helpType="custom"
i18n-customHtml
customHtml="
Write directly CSS code. Example:<br />
<pre>
body {{ '{' }}
background-color: red;
{{ '}' }}
</pre>
<div class="form-group">
<label for="customizationCSS">CSS</label>
<my-help
helpType="custom"
i18n-customHtml
customHtml="
Write directly CSS code. Example:<br />
<pre>
body {{ '{' }}
background-color: red;
{{ '}' }}
</pre>
Prepend with <em>#custom-css</em> to override styles. Example:
<pre>
#custom-css .logged-in-email {{ '{' }}
color: red;
{{ '}' }}
</pre>
"
></my-help>
<textarea
id="customizationCSS" formControlName="css"
[ngClass]="{ 'input-error': formErrors['instance.customizations.css'] }"
></textarea>
<div *ngIf="formErrors.instance.customizations.css" class="form-error">{{ formErrors.instance.customizations.css }}</div>
</div>
</ng-container>
</ng-container>
Prepend with <em>#custom-css</em> to override styles. Example:
<pre>
#custom-css .logged-in-email {{ '{' }}
color: red;
{{ '}' }}
</pre>
"
></my-help>
<textarea
id="customizationCSS" formControlName="customizationCSS"
[ngClass]="{ 'input-error': formErrors['customizationCSS'] }"
></textarea>
<div *ngIf="formErrors.customizationCSS" class="form-error">
{{ formErrors.customizationCSS }}
</div>
</div>
</ng-template>
</ngb-tab>
</ngb-tabset>

View File

@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'
import { ConfigService } from '@app/+admin/config/shared/config.service'
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'
@ -18,14 +18,11 @@ 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 i18n: I18n
@ -58,40 +55,78 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
}
getResolutionKey (resolution: string) {
return 'transcodingResolution' + resolution
return 'transcoding.resolutions.' + resolution
}
ngOnInit () {
const formGroupData: { [key: string]: any } = {
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)
@ -101,90 +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 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
@ -194,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: { [key: string]: any } = {
instanceName: this.customConfig.instance.name,
instanceShortDescription: this.customConfig.instance.shortDescription,
instanceDescription: this.customConfig.instance.description,
instanceTerms: this.customConfig.instance.terms,
instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
servicesTwitterUsername: this.customConfig.services.twitter.username,
servicesTwitterWhitelisted: this.customConfig.services.twitter.whitelisted,
cachePreviewsSize: this.customConfig.cache.previews.size,
cacheCaptionsSize: this.customConfig.cache.captions.size,
signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit,
signupRequiresEmailVerification: this.customConfig.signup.requiresEmailVerification,
adminEmail: this.customConfig.admin.email,
userVideoQuota: this.customConfig.user.videoQuota,
userVideoQuotaDaily: this.customConfig.user.videoQuotaDaily,
transcodingThreads: this.customConfig.transcoding.threads,
transcodingEnabled: this.customConfig.transcoding.enabled,
customizationJavascript: this.customConfig.instance.customizations.javascript,
customizationCSS: this.customConfig.instance.customizations.css,
importVideosHttpEnabled: this.customConfig.import.videos.http.enabled,
importVideosTorrentEnabled: this.customConfig.import.videos.torrent.enabled
}
for (const resolution of this.resolutions) {
const key = this.getResolutionKey(resolution)
data[key] = this.customConfig.transcoding.resolutions[resolution]
}
this.form.patchValue(data)
this.form.patchValue(this.customConfig)
}
}

View File

@ -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
) {
@ -32,14 +32,14 @@ export class FollowersListComponent extends RestTable implements OnInit {
}
protected loadData () {
this.followService.getFollowers(this.pagination, this.sort)
this.followService.getFollowers(this.pagination, this.sort, this.search)
.subscribe(
resultList => {
this.followers = resultList.data
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,6 +1,6 @@
import { Component } from '@angular/core'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { ConfirmService } from '../../../core'
import { validateHost } from '../../../shared'
import { FollowService } from '../shared'
@ -18,7 +18,7 @@ export class FollowingAddComponent {
constructor (
private router: Router,
private notificationsService: NotificationsService,
private notifier: Notifier,
private confirmService: ConfirmService,
private followService: FollowService,
private i18n: I18n
@ -64,12 +64,12 @@ export class FollowingAddComponent {
this.followService.follow(hosts).subscribe(
() => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Follow request(s) sent!'))
this.notifier.success(this.i18n('Follow request(s) sent!'))
setTimeout(() => this.router.navigate([ '/admin/follows/following-list' ]), 500)
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}

View File

@ -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
@ -41,14 +41,11 @@ 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)
)
}
@ -60,7 +57,7 @@ export class FollowingListComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

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

View File

@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core'
import { peertubeLocalStorage } from '@app/shared/misc/peertube-local-storage'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { SortMeta } from 'primeng/primeng'
import { Job } from '../../../../../../shared/index'
import { JobState } from '../../../../../../shared/models'
@ -25,7 +25,7 @@ export class JobsListComponent extends RestTable implements OnInit {
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private jobsService: JobService,
private i18n: I18n
) {
@ -53,7 +53,7 @@ export class JobsListComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
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, AccountBlock } from '@app/shared/blocklist'
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
@Component({
selector: 'my-instance-account-blocklist',
@ -18,7 +18,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private blocklistService: BlocklistService,
private i18n: I18n
) {
@ -35,8 +35,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
this.blocklistService.unblockAccountByInstance(blockedAccount)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.notifier.success(
this.i18n('Account {{nameWithHost}} unmuted by your instance.', { nameWithHost: blockedAccount.nameWithHost })
)
@ -53,7 +52,7 @@ export class InstanceAccountBlocklistComponent extends RestTable implements OnIn
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { RestPagination, RestTable } from '@app/shared'
import { SortMeta } from 'primeng/components/common/sortmeta'
@ -19,7 +19,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private blocklistService: BlocklistService,
private i18n: I18n
) {
@ -36,10 +36,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
this.blocklistService.unblockServerByInstance(host)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Instance {{host}} unmuted by your instance.', { host })
)
this.notifier.success(this.i18n('Instance {{host}} unmuted by your instance.', { host }))
this.loadData()
}
@ -54,7 +51,7 @@ export class InstanceServerBlocklistComponent extends RestTable implements OnIni
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

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

View File

@ -1,7 +1,8 @@
<ng-template #modal>
<div class="modal-header">
<h4 i18n class="modal-title">Moderation comment</h4>
<span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
</div>
<div class="modal-body">
@ -14,12 +15,12 @@
</div>
</div>
<div i18n>
<div class="form-group" i18n>
This comment can only be seen by you or the other moderators.
</div>
<div class="form-group inputs">
<span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span>
<span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
<input
type="submit" i18n-value value="Update this comment" class="action-button-submit"
@ -29,4 +30,4 @@
</form>
</div>
</ng-template>
</ng-template>

View File

@ -1,5 +1,5 @@
import { Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { FormReactive, VideoAbuseService, VideoAbuseValidatorsService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@ -22,7 +22,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
constructor (
protected formValidatorService: FormValidatorService,
private modalService: NgbModal,
private notificationsService: NotificationsService,
private notifier: Notifier,
private videoAbuseService: VideoAbuseService,
private videoAbuseValidatorsService: VideoAbuseValidatorsService,
private i18n: I18n
@ -45,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)
)
}
}

View File

@ -41,7 +41,7 @@
</td>
<td class="action-cell">
<my-action-dropdown i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
<my-action-dropdown placement="bottom-right" i18n-label label="Actions" [actions]="videoAbuseActions" [entry]="videoAbuse"></my-action-dropdown>
</td>
</tr>
</ng-template>
@ -51,15 +51,15 @@
<td class="moderation-expanded" colspan="6">
<div>
<span i18n class="moderation-expanded-label">Reason:</span>
<span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
</div>
<div *ngIf="videoAbuse.moderationComment">
<span i18n class="moderation-expanded-label">Moderation comment:</span>
<span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
</div>
</td>
</tr>
</ng-template>
</p-table>
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>
<my-moderation-comment-modal #moderationCommentModal (commentUpdated)="onModerationCommentUpdated()"></my-moderation-comment-modal>

View File

@ -1,6 +1,6 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { Account } from '../../../shared/account/account.model'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { VideoAbuse, VideoAbuseState } from '../../../../../../shared'
import { RestPagination, RestTable, VideoAbuseService } from '../../../shared'
@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
import { ConfirmService } from '../../../core/index'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { Video } from '../../../shared/video/video.model'
import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-abuse-list',
@ -27,10 +28,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
videoAbuseActions: DropdownAction<VideoAbuse>[] = []
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private videoAbuseService: VideoAbuseService,
private confirmService: ConfirmService,
private i18n: I18n
private i18n: I18n,
private markdownRenderer: MarkdownService
) {
super()
@ -90,14 +92,11 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
this.videoAbuseService.removeVideoAbuse(videoAbuse).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Abuse deleted.')
)
this.notifier.success(this.i18n('Abuse deleted.'))
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
@ -106,11 +105,15 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
.subscribe(
() => this.loadData(),
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
toHtml (text: string) {
return this.markdownRenderer.textMarkdownToHTML(text)
}
protected loadData () {
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
.subscribe(
@ -119,7 +122,7 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -7,6 +7,7 @@
<th style="width: 40px"></th>
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
<th i18n>Sensitive</th>
<th i18n>Unfederated</th>
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 120px;"></th>
</tr>
@ -26,20 +27,21 @@
</a>
</td>
<td>{{ videoBlacklist.video.nsfw }}</td>
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
<td>{{ videoBlacklist.createdAt }}</td>
<td class="action-cell">
<my-action-dropdown i18n-label label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
<my-action-dropdown i18n-label placement="bottom-right" label="Actions" [actions]="videoBlacklistActions" [entry]="videoBlacklist"></my-action-dropdown>
</td>
</tr>
</ng-template>
<ng-template pTemplate="rowexpansion" let-videoBlacklist>
<tr>
<td class="moderation-expanded" colspan="5">
<td class="moderation-expanded" colspan="6">
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
<span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span>
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
</td>
</tr>
</ng-template>

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { ConfirmService } from '../../../core'
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
import { VideoBlacklist } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
import { Video } from '../../../shared/video/video.model'
import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-blacklist-list',
@ -23,9 +24,10 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private confirmService: ConfirmService,
private videoBlacklistService: VideoBlacklistService,
private markdownRenderer: MarkdownService,
private i18n: I18n
) {
super()
@ -46,6 +48,16 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
return Video.buildClientUrl(videoBlacklist.video.uuid)
}
booleanToText (value: boolean) {
if (value === true) return this.i18n('yes')
return this.i18n('no')
}
toHtml (text: string) {
return this.markdownRenderer.textMarkdownToHTML(text)
}
async removeVideoFromBlacklist (entry: VideoBlacklist) {
const confirmMessage = this.i18n(
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
@ -56,14 +68,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
this.videoBlacklistService.removeVideoFromBlacklist(entry.video.id).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name })
)
this.notifier.success(this.i18n('Video {{name}} removed from the blacklist.', { name: entry.video.name }))
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
@ -75,7 +84,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { ServerService } from '../../../core'
import { Notifier, ServerService } from '@app/core'
import { UserCreate, UserRole } from '../../../../../../shared'
import { UserEdit } from './user-edit'
import { I18n } from '@ngx-translate/i18n-polyfill'
@ -24,7 +23,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
protected configService: ConfigService,
private userValidatorsService: UserValidatorsService,
private router: Router,
private notificationsService: NotificationsService,
private notifier: Notifier,
private userService: UserService,
private i18n: I18n
) {
@ -60,10 +59,7 @@ export class UserCreateComponent extends UserEdit implements OnInit {
this.userService.addUser(userCreate).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('User {{username}} created.', { username: userCreate.username })
)
this.notifier.success(this.i18n('User {{username}} created.', { username: userCreate.username }))
this.router.navigate([ '/admin/users/list' ])
},

View File

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

View File

@ -2,7 +2,7 @@
<div i18n class="form-sub-title">Users list</div>
<a class="add-button" routerLink="/admin/users/create">
<span class="icon icon-add"></span>
<my-global-icon iconName="add"></my-global-icon>
<ng-container i18n>Create user</ng-container>
</a>
</div>
@ -65,7 +65,9 @@
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
</a>
</td>
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
<ng-template #emailWithVerificationStatus>
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
<em>? {{ user.email }}</em>
@ -76,6 +78,7 @@
</td>
</ng-template>
</ng-template>
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
<td>{{ user.roleLabel }}</td>
<td>{{ user.createdAt }}</td>

View File

@ -2,7 +2,7 @@
@import '_mixins';
.add-button {
@include create-button('../../../../assets/images/global/add.svg');
@include create-button;
}
tr.banned {
@ -23,4 +23,4 @@ tr.banned {
input {
@include peertube-input-text(250px);
}
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { ConfirmService, ServerService } from '../../../core'
import { RestPagination, RestTable, UserService } from '../../../shared'
@ -26,7 +26,7 @@ export class UserListComponent extends RestTable implements OnInit {
bulkUserActions: DropdownAction<User[]>[] = []
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private confirmService: ConfirmService,
private serverService: ServerService,
private userService: UserService,
@ -68,7 +68,7 @@ export class UserListComponent extends RestTable implements OnInit {
openBanUserModal (users: User[]) {
for (const user of users) {
if (user.username === 'root') {
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot ban root.'))
this.notifier.error(this.i18n('You cannot ban root.'))
return
}
}
@ -91,18 +91,18 @@ export class UserListComponent extends RestTable implements OnInit {
() => {
const message = this.i18n('{{num}} users unbanned.', { num: users.length })
this.notificationsService.success(this.i18n('Success'), message)
this.notifier.success(message)
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
async removeUsers (users: User[]) {
for (const user of users) {
if (user.username === 'root') {
this.notificationsService.error(this.i18n('Error'), this.i18n('You cannot delete root.'))
this.notifier.error(this.i18n('You cannot delete root.'))
return
}
}
@ -113,28 +113,22 @@ export class UserListComponent extends RestTable implements OnInit {
this.userService.removeUser(users).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('{{num}} users deleted.', { num: users.length })
)
this.notifier.success(this.i18n('{{num}} users deleted.', { num: users.length }))
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
async setEmailsAsVerified (users: User[]) {
this.userService.updateUsers(users, { emailVerified: true }).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('{{num}} users email set as verified.', { num: users.length })
)
this.notifier.success(this.i18n('{{num}} users email set as verified.', { num: users.length }))
this.loadData()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
@ -146,13 +140,13 @@ export class UserListComponent extends RestTable implements OnInit {
this.selectedUsers = []
this.userService.getUsers(this.pagination, this.sort, this.search)
.subscribe(
resultList => {
this.users = resultList.data
this.totalRecords = resultList.total
},
.subscribe(
resultList => {
this.users = resultList.data
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,9 +1,9 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
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, AccountBlock } from '@app/shared/blocklist'
import { AccountBlock, BlocklistService } from '@app/shared/blocklist'
@Component({
selector: 'my-account-blocklist',
@ -18,7 +18,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private blocklistService: BlocklistService,
private i18n: I18n
) {
@ -35,10 +35,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
this.blocklistService.unblockAccountByUser(blockedAccount)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost })
)
this.notifier.success(this.i18n('Account {{nameWithHost}} unmuted.', { nameWithHost: blockedAccount.nameWithHost }))
this.loadData()
}
@ -53,7 +50,7 @@ export class MyAccountBlocklistComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
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'
@ -19,7 +19,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
constructor (
private notificationsService: NotificationsService,
private notifier: Notifier,
private blocklistService: BlocklistService,
private i18n: I18n
) {
@ -36,10 +36,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
this.blocklistService.unblockServerByUser(host)
.subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Instance {{host}} unmuted.', { host })
)
this.notifier.success(this.i18n('Instance {{host}} unmuted.', { host }))
this.loadData()
}
@ -54,7 +51,7 @@ export class MyAccountServerBlocklistComponent extends RestTable implements OnIn
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -0,0 +1,27 @@
<div class="top-buttons">
<div class="history-switch">
<p-inputSwitch [(ngModel)]="videosHistoryEnabled" (ngModelChange)="onVideosHistoryChange()"></p-inputSwitch>
<label i18n>History enabled</label>
</div>
<div class="delete-history">
<button (click)="deleteHistory()" i18n>Delete history</button>
</div>
</div>
<div class="no-history" i18n *ngIf="pagination.totalItems === 0">You don't have videos history yet.</div>
<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
<div *ngFor="let videos of videoPages;" class="videos-page">
<div class="video" *ngFor="let video of videos">
<my-video-thumbnail [video]="video"></my-video-thumbnail>
<div class="video-info">
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
<a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,99 @@
@import '_variables';
@import '_mixins';
.no-history {
display: flex;
justify-content: center;
margin-top: 50px;
font-weight: $font-semibold;
font-size: 16px;
}
.top-buttons {
margin-bottom: 20px;
display: flex;
.history-switch {
display: flex;
flex-grow: 1;
label {
margin: 0 0 0 5px;
}
}
.delete-history {
font-size: 15px;
button {
@include peertube-button;
@include grey-button;
}
}
}
.video {
@include row-blocks;
my-video-thumbnail {
margin-right: 10px;
}
.video-info {
flex-grow: 1;
.video-info-name {
@include disable-default-a-behaviour;
color: var(--mainForegroundColor);
display: block;
width: fit-content;
font-size: 18px;
font-weight: $font-semibold;
}
.video-info-date-views {
font-size: 14px;
}
.video-info-account {
@include disable-default-a-behaviour;
display: block;
width: fit-content;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
color: $grey-foreground-color;
&:hover {
color: $grey-foreground-hover-color;
}
}
}
}
@media screen and (max-width: $small-view) {
.video {
flex-direction: column;
height: auto;
text-align: center;
.video-info-name {
margin: auto;
}
input[type=checkbox] {
display: none;
}
my-video-thumbnail {
margin-right: 0;
}
.video-buttons {
margin-top: 10px;
}
}
}

View File

@ -0,0 +1,107 @@
import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
import { VideoService } from '../../shared/video/video.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { ScreenService } from '@app/shared/misc/screen.service'
import { UserHistoryService } from '@app/shared/users/user-history.service'
import { UserService } from '@app/shared'
import { Notifier } from '@app/core'
@Component({
selector: 'my-account-history',
templateUrl: './my-account-history.component.html',
styleUrls: [ './my-account-history.component.scss' ]
})
export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
titlePage: string
currentRoute = '/my-account/history/videos'
pagination: ComponentPagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: null
}
videosHistoryEnabled: boolean
protected baseVideoWidth = -1
protected baseVideoHeight = 155
constructor (
protected router: Router,
protected route: ActivatedRoute,
protected authService: AuthService,
protected userService: UserService,
protected notifier: Notifier,
protected location: Location,
protected screenService: ScreenService,
protected i18n: I18n,
private confirmService: ConfirmService,
private videoService: VideoService,
private userHistoryService: UserHistoryService
) {
super()
this.titlePage = this.i18n('My videos history')
}
ngOnInit () {
super.ngOnInit()
this.videosHistoryEnabled = this.authService.getUser().videosHistoryEnabled
}
ngOnDestroy () {
super.ngOnDestroy()
}
getVideosObservable (page: number) {
const newPagination = immutableAssign(this.pagination, { currentPage: page })
return this.userHistoryService.getUserVideosHistory(newPagination)
}
generateSyndicationList () {
throw new Error('Method not implemented.')
}
onVideosHistoryChange () {
this.userService.updateMyProfile({ videosHistoryEnabled: this.videosHistoryEnabled })
.subscribe(
() => {
const message = this.videosHistoryEnabled === true ?
this.i18n('Videos history is enabled') :
this.i18n('Videos history is disabled')
this.notifier.success(message)
this.authService.refreshUserInformation()
},
err => this.notifier.error(err.message)
)
}
async deleteHistory () {
const title = this.i18n('Delete videos history')
const message = this.i18n('Are you sure you want to delete all your videos history?')
const res = await this.confirmService.confirm(message, title)
if (res !== true) return
this.userHistoryService.deleteUserVideosHistory()
.subscribe(
() => {
this.notifier.success(this.i18n('Videos history deleted'))
this.reloadVideos()
},
err => this.notifier.error(err.message)
)
}
}

View File

@ -0,0 +1,13 @@
<div class="header">
<a routerLink="/my-account/settings" fragment="notifications" i18n>
<my-global-icon iconName="cog"></my-global-icon>
Notification preferences
</a>
<button (click)="markAllAsRead()" i18n>
<my-global-icon iconName="circle-tick"></my-global-icon>
Mark all as read
</button>
</div>
<my-user-notifications #userNotification></my-user-notifications>

View File

@ -0,0 +1,25 @@
@import '_variables';
@import '_mixins';
.header {
display: flex;
justify-content: space-between;
font-size: 15px;
margin-bottom: 20px;
a {
@include peertube-button-link;
@include grey-button;
@include button-with-icon(18px, 3px, -1px);
}
button {
@include peertube-button;
@include grey-button;
@include button-with-icon(20px, 3px, -1px);
}
}
my-user-notifications {
font-size: 15px;
}

View File

@ -0,0 +1,14 @@
import { Component, ViewChild } from '@angular/core'
import { UserNotificationsComponent } from '@app/shared'
@Component({
templateUrl: './my-account-notifications.component.html',
styleUrls: [ './my-account-notifications.component.scss' ]
})
export class MyAccountNotificationsComponent {
@ViewChild('userNotification') userNotification: UserNotificationsComponent
markAllAsRead () {
this.userNotification.markAllAsRead()
}
}

View File

@ -1,7 +1,8 @@
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-header">
<h4 i18n class="modal-title">Accept ownership</h4>
<span class="close" aria-label="Close" role="button" (click)="dismiss()"></span>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
</div>
<div class="modal-body" [formGroup]="form">

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, EventEmitter, OnInit, Output, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { AuthService, Notifier } from '@app/core'
import { FormReactive } from '@app/shared'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoOwnershipService } from '@app/shared/video-ownership'
@ -8,7 +8,6 @@ import { VideoAcceptOwnershipValidatorsService } from '@app/shared/forms/form-va
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { AuthService } from '@app/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
@Component({
@ -31,7 +30,7 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
protected formValidatorService: FormValidatorService,
private videoChangeOwnershipValidatorsService: VideoAcceptOwnershipValidatorsService,
private videoOwnershipService: VideoOwnershipService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private authService: AuthService,
private videoChannelService: VideoChannelService,
private modalService: NgbModal,
@ -68,12 +67,12 @@ export class MyAccountAcceptOwnershipComponent extends FormReactive implements O
.acceptOwnership(videoChangeOwnership.id, { channelId: channel })
.subscribe(
() => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Ownership accepted'))
this.notifier.success(this.i18n('Ownership accepted'))
if (this.accepted) this.accepted.emit()
this.videoChangeOwnership = undefined
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -40,10 +40,10 @@
<td class="action-cell">
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
<my-button i18n label="Accept"
icon="icon-tick"
icon="tick"
(click)="openAcceptModal(videoChangeOwnership)"></my-button>
<my-button i18n label="Refuse"
icon="icon-cross"
icon="cross"
(click)="refuse(videoChangeOwnership)">Refuse</my-button>
</ng-container>
</td>

View File

@ -1,13 +1,11 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Notifier } from '@app/core'
import { RestPagination, RestTable } from '@app/shared'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { VideoChangeOwnership } from '../../../../../shared'
import { VideoOwnershipService } from '@app/shared/video-ownership'
import { Account } from '@app/shared/account/account.model'
import { MyAccountAcceptOwnershipComponent }
from '@app/+my-account/my-account-ownership/my-account-accept-ownership/my-account-accept-ownership.component'
import { MyAccountAcceptOwnershipComponent } from './my-account-accept-ownership/my-account-accept-ownership.component'
@Component({
selector: 'my-account-ownership',
@ -23,9 +21,8 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
@ViewChild('myAccountAcceptOwnershipComponent') myAccountAcceptOwnershipComponent: MyAccountAcceptOwnershipComponent
constructor (
private notificationsService: NotificationsService,
private videoOwnershipService: VideoOwnershipService,
private i18n: I18n
private notifier: Notifier,
private videoOwnershipService: VideoOwnershipService
) {
super()
}
@ -50,7 +47,7 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
this.videoOwnershipService.refuseOwnership(videoChangeOwnership.id)
.subscribe(
() => this.loadData(),
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
@ -62,7 +59,7 @@ export class MyAccountOwnershipComponent extends RestTable implements OnInit {
this.totalRecords = resultList.total
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -13,6 +13,8 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
import { MyAccountNotificationsComponent } from '@app/+my-account/my-account-notifications/my-account-notifications.component'
const myAccountRoutes: Routes = [
{
@ -114,6 +116,24 @@ const myAccountRoutes: Routes = [
title: 'Muted instances'
}
}
},
{
path: 'history/videos',
component: MyAccountHistoryComponent,
data: {
meta: {
title: 'Videos history'
}
}
},
{
path: 'notifications',
component: MyAccountNotificationsComponent,
data: {
meta: {
title: 'Notifications'
}
}
}
]
}

View File

@ -1,11 +1,10 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { AuthService, Notifier } from '@app/core'
import { FormReactive, UserService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
import { filter } from 'rxjs/operators'
import { AuthService } from '@app/core'
import { User } from '../../../../../../shared'
@Component({
@ -20,7 +19,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
constructor (
protected formValidatorService: FormValidatorService,
private userValidatorsService: UserValidatorsService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private authService: AuthService,
private userService: UserService,
private i18n: I18n
@ -50,7 +49,7 @@ export class MyAccountChangePasswordComponent extends FormReactive implements On
this.userService.changePassword(currentPassword, newPassword).subscribe(
() => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Password updated.'))
this.notifier.success(this.i18n('Password updated.'))
this.form.reset()
this.error = null

View File

@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { AuthService, ConfirmService, RedirectService } from '../../../core'
import { UserService } from '../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
@ -15,7 +15,7 @@ export class MyAccountDangerZoneComponent {
constructor (
private authService: AuthService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private userService: UserService,
private confirmService: ConfirmService,
private redirectService: RedirectService,
@ -34,13 +34,13 @@ export class MyAccountDangerZoneComponent {
this.userService.deleteMe().subscribe(
() => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Your account is deleted.'))
this.notifier.success(this.i18n('Your account is deleted.'))
this.authService.logout()
this.redirectService.redirectToHomepage()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

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

View File

@ -0,0 +1,19 @@
<div class="custom-row">
<div i18n>Activities</div>
<div i18n>Web</div>
<div i18n *ngIf="emailEnabled">Email</div>
</div>
<div class="custom-row" *ngFor="let notificationType of notificationSettingKeys">
<ng-container *ngIf="hasUserRight(notificationType)">
<div>{{ labelNotifications[notificationType] }}</div>
<div>
<p-inputSwitch [(ngModel)]="webNotifications[notificationType]" (onChange)="updateWebSetting(notificationType, $event.checked)"></p-inputSwitch>
</div>
<div *ngIf="emailEnabled">
<p-inputSwitch [(ngModel)]="emailNotifications[notificationType]" (onChange)="updateEmailSetting(notificationType, $event.checked)"></p-inputSwitch>
</div>
</ng-container>
</div>

View File

@ -0,0 +1,25 @@
@import '_variables';
@import '_mixins';
.custom-row {
display: flex;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.10);
&:first-child {
font-size: 16px;
& > div {
font-weight: $font-semibold;
}
}
& > div {
width: 350px;
}
& > div {
padding: 10px
}
}

View File

@ -0,0 +1,99 @@
import { Component, Input, OnInit } from '@angular/core'
import { User } from '@app/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subject } from 'rxjs'
import { UserNotificationSetting, UserNotificationSettingValue, UserRight } from '../../../../../../shared'
import { Notifier, ServerService } from '@app/core'
import { debounce } from 'lodash-es'
import { UserNotificationService } from '@app/shared/users/user-notification.service'
@Component({
selector: 'my-account-notification-preferences',
templateUrl: './my-account-notification-preferences.component.html',
styleUrls: [ './my-account-notification-preferences.component.scss' ]
})
export class MyAccountNotificationPreferencesComponent implements OnInit {
@Input() user: User = null
@Input() userInformationLoaded: Subject<any>
notificationSettingKeys: (keyof UserNotificationSetting)[] = []
emailNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
webNotifications: { [ id in keyof UserNotificationSetting ]: boolean } = {} as any
labelNotifications: { [ id in keyof UserNotificationSetting ]: string } = {} as any
rightNotifications: { [ id in keyof Partial<UserNotificationSetting> ]: UserRight } = {} as any
emailEnabled: boolean
private savePreferences = debounce(this.savePreferencesImpl.bind(this), 500)
constructor (
private i18n: I18n,
private userNotificationService: UserNotificationService,
private serverService: ServerService,
private notifier: Notifier
) {
this.labelNotifications = {
newVideoFromSubscription: this.i18n('New video from your subscriptions'),
newCommentOnMyVideo: this.i18n('New comment on your video'),
videoAbuseAsModerator: this.i18n('New video abuse on local video'),
blacklistOnMyVideo: this.i18n('One of your video is blacklisted/unblacklisted'),
myVideoPublished: this.i18n('Video published (after transcoding/scheduled update)'),
myVideoImportFinished: this.i18n('Video import finished'),
newUserRegistration: this.i18n('A new user registered on your instance'),
newFollow: this.i18n('You or your channel(s) has a new follower'),
commentMention: this.i18n('Someone mentioned you in video comments')
}
this.notificationSettingKeys = Object.keys(this.labelNotifications) as (keyof UserNotificationSetting)[]
this.rightNotifications = {
videoAbuseAsModerator: UserRight.MANAGE_VIDEO_ABUSES,
newUserRegistration: UserRight.MANAGE_USERS
}
this.emailEnabled = this.serverService.getConfig().email.enabled
}
ngOnInit () {
this.userInformationLoaded.subscribe(() => this.loadNotificationSettings())
}
hasUserRight (field: keyof UserNotificationSetting) {
const rightToHave = this.rightNotifications[field]
if (!rightToHave) return true // No rights needed
return this.user.hasRight(rightToHave)
}
updateEmailSetting (field: keyof UserNotificationSetting, value: boolean) {
if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.EMAIL
else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.EMAIL
this.savePreferences()
}
updateWebSetting (field: keyof UserNotificationSetting, value: boolean) {
if (value === true) this.user.notificationSettings[field] |= UserNotificationSettingValue.WEB
else this.user.notificationSettings[field] &= ~UserNotificationSettingValue.WEB
this.savePreferences()
}
private savePreferencesImpl () {
this.userNotificationService.updateNotificationSettings(this.user, this.user.notificationSettings)
.subscribe(
() => {
this.notifier.success(this.i18n('Preferences saved'), undefined, 2000)
},
err => this.notifier.error(err.message)
)
}
private loadNotificationSettings () {
for (const key of Object.keys(this.user.notificationSettings)) {
const value = this.user.notificationSettings[key]
this.emailNotifications[key] = value & UserNotificationSettingValue.EMAIL
this.webNotifications[key] = value & UserNotificationSettingValue.WEB
}
}
}

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { FormReactive, UserService } from '../../../shared'
import { User } from '@app/shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
@ -21,7 +21,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
constructor (
protected formValidatorService: FormValidatorService,
private userValidatorsService: UserValidatorsService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private userService: UserService,
private i18n: I18n
) {
@ -53,7 +53,7 @@ export class MyAccountProfileComponent extends FormReactive implements OnInit {
this.user.account.displayName = displayName
this.user.account.description = description
this.notificationsService.success(this.i18n('Success'), this.i18n('Profile updated.'))
this.notifier.success(this.i18n('Profile updated.'))
},
err => this.error = err.message

View File

@ -4,10 +4,11 @@
<span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
</div>
<ng-template [ngIf]="user && user.account">
<div i18n class="account-title">Profile</div>
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
</ng-template>
<div i18n class="account-title">Profile</div>
<my-account-profile [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-profile>
<div i18n class="account-title" id="notifications">Notifications</div>
<my-account-notification-preferences [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-notification-preferences>
<div i18n class="account-title">Password</div>
<my-account-change-password></my-account-change-password>
@ -16,4 +17,4 @@
<my-account-video-settings [user]="user" [userInformationLoaded]="userInformationLoaded"></my-account-video-settings>
<div i18n class="account-title">Danger zone</div>
<my-account-danger-zone [user]="user"></my-account-danger-zone>
<my-account-danger-zone [user]="user"></my-account-danger-zone>

View File

@ -1,5 +1,5 @@
import { Component, OnInit, ViewChild } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { BytesPipe } from 'ngx-pipes'
import { AuthService } from '../../core'
import { User } from '../../shared'
@ -19,7 +19,7 @@ export class MyAccountSettingsComponent implements OnInit {
constructor (
private userService: UserService,
private authService: AuthService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private i18n: I18n
) {}
@ -48,12 +48,12 @@ export class MyAccountSettingsComponent implements OnInit {
this.userService.changeAvatar(formData)
.subscribe(
data => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
this.notifier.success(this.i18n('Avatar changed.'))
this.user.updateAccountAvatar(data.avatar)
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -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
) {
@ -54,12 +54,12 @@ export class MyAccountVideoSettingsComponent extends FormReactive implements OnI
this.userService.updateMyProfile(details).subscribe(
() => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Information updated.'))
this.notifier.success(this.i18n('Information updated.'))
this.authService.refreshUserInformation()
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}
}

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { UserSubscriptionService } from '@app/shared/user-subscription'
@ -21,7 +21,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
constructor (
private userSubscriptionService: UserSubscriptionService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private i18n: I18n
) {}
@ -37,7 +37,7 @@ export class MyAccountSubscriptionsComponent implements OnInit {
this.pagination.totalItems = res.total
},
error => this.notificationsService.error(this.i18n('Error'), error.message)
error => this.notifier.error(error.message)
)
}

View File

@ -1,10 +1,9 @@
import { Component, OnInit } from '@angular/core'
import { Router } from '@angular/router'
import { NotificationsService } from 'angular2-notifications'
import { AuthService, Notifier } from '@app/core'
import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
import { VideoChannelCreate } from '../../../../../shared/models/videos'
import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
import { AuthService } from '@app/core'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
@ -21,7 +20,7 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
protected formValidatorService: FormValidatorService,
private authService: AuthService,
private videoChannelValidatorsService: VideoChannelValidatorsService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private router: Router,
private videoChannelService: VideoChannelService,
private i18n: I18n
@ -56,8 +55,8 @@ export class MyAccountVideoChannelCreateComponent extends MyAccountVideoChannelE
this.videoChannelService.createVideoChannel(videoChannelCreate).subscribe(
() => {
this.authService.refreshUserInformation()
this.notificationsService.success(
this.i18n('Success'),
this.notifier.success(
this.i18n('Video channel {{videoChannelName}} created.', { videoChannelName: videoChannelCreate.displayName })
)
this.router.navigate([ '/my-account', 'video-channels' ])

View File

@ -1,12 +1,11 @@
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'
@ -26,7 +25,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
protected formValidatorService: FormValidatorService,
private authService: AuthService,
private videoChannelValidatorsService: VideoChannelValidatorsService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private router: Router,
private route: ActivatedRoute,
private videoChannelService: VideoChannelService,
@ -79,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' ])
},
@ -94,12 +94,12 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
.subscribe(
data => {
this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
this.notifier.success(this.i18n('Avatar changed.'))
this.videoChannelToUpdate.updateAvatar(data.avatar)
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
err => this.notifier.error(err.message)
)
}

View File

@ -1,6 +1,6 @@
<div class="video-channels-header">
<a class="create-button" routerLink="create">
<span class="icon icon-add"></span>
<my-global-icon iconName="add"></my-global-icon>
<ng-container i18n>Create another video channel</ng-container>
</a>
</div>

View File

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

View File

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'
import { NotificationsService } from 'angular2-notifications'
import { Notifier } from '@app/core'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
@ -20,7 +20,7 @@ export class MyAccountVideoChannelsComponent implements OnInit {
constructor (
private authService: AuthService,
private notificationsService: NotificationsService,
private notifier: Notifier,
private confirmService: ConfirmService,
private videoChannelService: VideoChannelService,
private i18n: I18n
@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
async deleteVideoChannel (videoChannel: VideoChannel) {
const res = await this.confirmService.confirmWithInput(
this.i18n(
'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.',
{ videoChannelName: videoChannel.displayName }
'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' +
'and you will not be able to create another channel with the same name ({{channelName}})!',
{ channelDisplayName: videoChannel.displayName, channelName: videoChannel.name }
),
this.i18n(
'Please type the display name of the video channel ({{displayName}}) to confirm',
{ displayName: videoChannel.displayName }
),
this.i18n('Please type the name of the video channel to confirm'),
videoChannel.displayName,
this.i18n('Delete')
)
@ -46,15 +50,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
this.videoChannelService.removeVideoChannel(videoChannel)
.subscribe(
status => {
() => {
this.loadVideoChannels()
this.notificationsService.success(
this.i18n('Success'),
this.notifier.success(
this.i18n('Video channel {{videoChannelName}} deleted.', { videoChannelName: videoChannel.displayName })
)
},
error => this.notificationsService.error(this.i18n('Error'), error.message)
error => this.notifier.error(error.message)
)
}

View File

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

View File

@ -32,7 +32,7 @@
</span>
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
<span class="icon icon-delete-white"></span>
<my-global-icon iconName="delete"></my-global-icon>
<ng-container i18n>Delete</ng-container>
</span>
</div>
@ -45,7 +45,7 @@
<my-button i18n-label label="Change ownership"
className="action-button-change-ownership"
icon="icon-im-with-her"
icon="im-with-her"
(click)="changeOwnership($event, video)"
></my-button>
</div>
@ -53,4 +53,4 @@
</div>
</div>
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>

View File

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

View File

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

View File

@ -1,7 +1,8 @@
<ng-template #modal let-close="close" let-dismiss="dismiss">
<div class="modal-header">
<h4 i18n class="modal-title">Change ownership</h4>
<span class="close" aria-label="Close" role="button" (click)="dismiss()"></span>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
</div>
<div class="modal-body" [formGroup]="form">

View File

@ -1,5 +1,5 @@
import { Component, ElementRef, 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
@ -53,11 +53,9 @@ export class VideoChangeOwnershipComponent extends FormReactive implements OnIni
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)
)
}
}

View File

@ -1,40 +1,5 @@
<div class="row">
<div class="sub-menu">
<a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
<div ngbDropdown class="my-library">
<span role="button" class="title-page" [ngClass]="{ active: libraryLabel !== '' }" ngbDropdownToggle>
<ng-container i18n>My library</ng-container>
<ng-container *ngIf="libraryLabel"> - {{ libraryLabel }}</ng-container>
</span>
<div ngbDropdownMenu>
<a class="dropdown-item" i18n routerLink="/my-account/video-channels">My channels</a>
<a class="dropdown-item" i18n routerLink="/my-account/videos">My videos</a>
<a class="dropdown-item" i18n routerLink="/my-account/subscriptions">My subscriptions</a>
<a class="dropdown-item" *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports">My imports</a>
</div>
</div>
<div ngbDropdown class="misc">
<span role="button" class="title-page" [ngClass]="{ active: miscLabel !== '' }" ngbDropdownToggle>
<ng-container i18n>Misc</ng-container>
<ng-container *ngIf="miscLabel"> - {{ miscLabel }}</ng-container>
</span>
<div ngbDropdownMenu>
<a class="dropdown-item" i18n routerLink="/my-account/blocklist/accounts">Muted accounts</a>
<a class="dropdown-item" i18n routerLink="/my-account/blocklist/servers">Muted instances</a>
<a class="dropdown-item" i18n routerLink="/my-account/ownership">Ownership changes</a>
</div>
</div>
</div>
<my-top-menu-dropdown [menuEntries]="menuEntries"></my-top-menu-dropdown>
<div class="margin-content">
<router-outlet></router-outlet>

View File

@ -1,14 +1,3 @@
.my-library, .misc {
span[role=button] {
cursor: pointer;
}
a {
display: block;
}
.row {
flex-direction: column;
}
/deep/ .dropdown-toggle::after {
position: relative;
top: 2px;
}

View File

@ -1,38 +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 = ''
miscLabel = ''
private routeSub: Subscription
export class MyAccountComponent {
menuEntries: TopMenuDropdownParam[] = []
constructor (
private serverService: ServerService,
private router: Router,
private i18n: I18n
) {}
) {
ngOnInit () {
this.updateLabels(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.updateLabels(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 () {
@ -41,27 +83,4 @@ export class MyAccountComponent implements OnInit, OnDestroy {
return importConfig.http.enabled || importConfig.torrent.enabled
}
private updateLabels (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 = ''
}
if (path.startsWith('/my-account/blocklist/accounts')) {
this.miscLabel = this.i18n('Muted accounts')
} else if (path.startsWith('/my-account/blocklist/servers')) {
this.miscLabel = this.i18n('Muted instances')
} else {
this.miscLabel = ''
}
}
}

View File

@ -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'
@ -21,6 +22,9 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin
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: [
@ -28,7 +32,8 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
MyAccountRoutingModule,
AutoCompleteModule,
SharedModule,
TableModule
TableModule,
InputSwitchModule
],
declarations: [
@ -49,7 +54,10 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
MyAccountDangerZoneComponent,
MyAccountSubscriptionsComponent,
MyAccountBlocklistComponent,
MyAccountServerBlocklistComponent
MyAccountServerBlocklistComponent,
MyAccountHistoryComponent,
MyAccountNotificationsComponent,
MyAccountNotificationPreferencesComponent
],
exports: [

View File

@ -1,8 +1,8 @@
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',
@ -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
}

View File

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

View File

@ -9,7 +9,7 @@
<ng-template #verificationError>
<div>
<span i18n>An error occurred. </span>
<a i18n routerLink="/verify-account/ask-email">Request new verification email.</a>
<a i18n routerLink="/verify-account/ask-send-email">Request new verification email.</a>
</div>
</ng-template>
</div>

View File

@ -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
@ -29,7 +29,7 @@ export class VerifyAccountEmailComponent implements OnInit {
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()
}
@ -46,7 +46,7 @@ export class VerifyAccountEmailComponent implements OnInit {
},
err => {
this.notificationsService.error(this.i18n('Error'), err.message)
this.notifier.error(err.message)
}
)
}

View File

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

View File

@ -2,7 +2,6 @@ import { Component, OnDestroy, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { Location } from '@angular/common'
import { immutableAssign } from '@app/shared/misc/utils'
import { NotificationsService } from 'angular2-notifications'
import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm'
import { AbstractVideoList } from '../../shared/video/abstract-video-list'
@ -13,6 +12,7 @@ import { tap } from 'rxjs/operators'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { ScreenService } from '@app/shared/misc/screen.service'
import { Notifier } from '@app/core'
@Component({
selector: 'my-video-channel-videos',
@ -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-channels/' + this.videoChannel.uuid + '/videos'
this.currentRoute = '/video-channels/' + this.videoChannel.nameWithHost + '/videos'
this.reloadVideos()
this.generateSyndicationList()

View File

@ -7,7 +7,7 @@ import { VideoChannelAboutComponent } from './video-channel-about/video-channel-
const videoChannelsRoutes: Routes = [
{
path: ':videoChannelId',
path: ':videoChannelName',
component: VideoChannelsComponent,
canActivateChild: [ MetaGuard ],
children: [

View File

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

View File

@ -43,7 +43,8 @@ const routes: Routes = [
imports: [
RouterModule.forRoot(routes, {
useHash: Boolean(history.pushState) === false,
preloadingStrategy: PreloadSelectedModulesList
preloadingStrategy: PreloadSelectedModulesList,
anchorScrolling: 'enabled'
})
],
providers: [

View File

@ -30,12 +30,27 @@
<footer class="row">
<a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a>&nbsp;-&nbsp;
<a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a>
<a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a>
</footer>
</div>
</div>
</div>
<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
<my-confirm></my-confirm>
<simple-notifications [options]="notificationOptions"></simple-notifications>
<p-toast position="bottom-right">
<ng-template let-message pTemplate="message">
<div class="notification-block">
<div class="message">
<h3>{{ message.summary }}</h3>
<p>{{ message.detail }}</p>
</div>
<span *ngIf="message.severity === 'success'" class="glyphicon glyphicon-ok"></span>
<span *ngIf="message.severity === 'info'" class="glyphicon glyphicon-info-sign"></span>
<span *ngIf="message.severity === 'error'" class="glyphicon glyphicon-remove"></span>
</div>
</ng-template>
</p-toast>

View File

@ -91,8 +91,3 @@ footer {
height: $footer-height;
justify-content: center;
}
simple-notifications {
position: relative;
z-index: 1500;
}

View File

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

View File

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

View File

@ -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,6 +71,7 @@ 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',
@ -89,7 +91,8 @@ export class AuthUser extends User {
role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole,
nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType,
webTorrentEnabled: peertubeLocalStorage.getItem(this.KEYS.WEBTORRENT_ENABLED) === 'true',
autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true'
autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true',
videosHistoryEnabled: peertubeLocalStorage.getItem(this.KEYS.VIDEOS_HISTORY_ENABLED) === 'true'
},
Tokens.load()
)
@ -104,12 +107,13 @@ export class AuthUser extends User {
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<ServerUserModel>, hashTokens: TokenOptions) {
super(userHash)
this.tokens = new Tokens(hashTokens)
}

View File

@ -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<AuthStatus>
userInformationLoaded = new ReplaySubject<boolean>(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 () {

View File

@ -1,4 +1,3 @@
export * from './auth-status.model'
export * from './auth-user.model'
export * from './auth.service'
export * from '../routing/login-guard.service'

View File

@ -1,2 +1 @@
export * from './confirm.component'
export * from './confirm.service'

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export * from './notifier.service'
export * from './user-notification-socket.service'

View File

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

View File

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

View File

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

View File

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

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