Merge branch 'develop' into pr/1285
This commit is contained in:
commit
b718fd2237
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -1,10 +1,99 @@
|
||||||
# Changelog
|
# 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
|
## v1.1.0
|
||||||
|
|
||||||
***Since v1.0.1***
|
***Since v1.0.1***
|
||||||
|
|
||||||
### BREAKING CHANGES
|
### BREAKING CHANGES
|
||||||
|
|
||||||
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
||||||
|
|
||||||
### Maintenance
|
### Maintenance
|
||||||
|
|
|
@ -133,7 +133,7 @@ You can also join the cheerful bunch that makes our community:
|
||||||
|
|
||||||
* Chat<a name="contact"></a>:
|
* Chat<a name="contact"></a>:
|
||||||
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
|
* 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:
|
* Forum:
|
||||||
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "peertube-client",
|
"name": "peertube-client",
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"licence": "GPLv3",
|
"licence": "GPLv3",
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -28,7 +28,8 @@
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"video.js": "^7",
|
"video.js": "^7",
|
||||||
"webtorrent/create-torrent/junk": "^1",
|
"webtorrent/create-torrent/junk": "^1",
|
||||||
"simple-get": "^2.8.1"
|
"simple-get": "^2.8.1",
|
||||||
|
"punycode": "^1.4.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
@ -63,20 +64,20 @@
|
||||||
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
|
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "~0.11.1",
|
"@angular-devkit/build-angular": "~0.13.1",
|
||||||
"@angular/animations": "~7.1.1",
|
"@angular/animations": "~7.2.4",
|
||||||
"@angular/cli": "~7.1.1",
|
"@angular/cli": "~7.3.1",
|
||||||
"@angular/common": "~7.1.1",
|
"@angular/common": "~7.2.4",
|
||||||
"@angular/compiler": "~7.1.1",
|
"@angular/compiler": "~7.2.4",
|
||||||
"@angular/compiler-cli": "~7.1.1",
|
"@angular/compiler-cli": "~7.2.4",
|
||||||
"@angular/core": "~7.1.1",
|
"@angular/core": "~7.2.4",
|
||||||
"@angular/forms": "~7.1.1",
|
"@angular/forms": "~7.2.4",
|
||||||
"@angular/http": "~7.1.1",
|
"@angular/http": "~7.2.4",
|
||||||
"@angular/language-service": "~7.1.1",
|
"@angular/language-service": "~7.2.4",
|
||||||
"@angular/platform-browser": "~7.1.1",
|
"@angular/platform-browser": "~7.2.4",
|
||||||
"@angular/platform-browser-dynamic": "~7.1.1",
|
"@angular/platform-browser-dynamic": "~7.2.4",
|
||||||
"@angular/router": "~7.1.1",
|
"@angular/router": "~7.2.4",
|
||||||
"@angular/service-worker": "~7.1.1",
|
"@angular/service-worker": "~7.2.4",
|
||||||
"@angularclass/hmr": "^2.1.3",
|
"@angularclass/hmr": "^2.1.3",
|
||||||
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^4.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^4.0.0",
|
||||||
|
@ -85,7 +86,9 @@
|
||||||
"@ngx-loading-bar/router": "^3.0.0",
|
"@ngx-loading-bar/router": "^3.0.0",
|
||||||
"@ngx-meta/core": "^6.0.0-rc.1",
|
"@ngx-meta/core": "^6.0.0-rc.1",
|
||||||
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
||||||
|
"@streamroot/videojs-hlsjs-plugin": "^1.0.7",
|
||||||
"@types/core-js": "^2.5.0",
|
"@types/core-js": "^2.5.0",
|
||||||
|
"@types/hls.js": "^0.12.0",
|
||||||
"@types/jasmine": "^2.8.7",
|
"@types/jasmine": "^2.8.7",
|
||||||
"@types/jasminewd2": "^2.0.3",
|
"@types/jasminewd2": "^2.0.3",
|
||||||
"@types/jest": "^23.3.1",
|
"@types/jest": "^23.3.1",
|
||||||
|
@ -109,6 +112,7 @@
|
||||||
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"focus-visible": "^4.1.5",
|
"focus-visible": "^4.1.5",
|
||||||
|
"hls.js": "^0.12.2",
|
||||||
"html-loader": "^0.5.5",
|
"html-loader": "^0.5.5",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"https-browserify": "^1.0.0",
|
"https-browserify": "^1.0.0",
|
||||||
|
@ -131,6 +135,7 @@
|
||||||
"ngx-qrcode2": "^0.0.9",
|
"ngx-qrcode2": "^0.0.9",
|
||||||
"node-sass": "^4.9.3",
|
"node-sass": "^4.9.3",
|
||||||
"npm-font-source-sans-pro": "^1.0.2",
|
"npm-font-source-sans-pro": "^1.0.2",
|
||||||
|
"p2p-media-loader-hlsjs": "^0.4.0",
|
||||||
"path-browserify": "^1.0.0",
|
"path-browserify": "^1.0.0",
|
||||||
"primeng": "^7.0.0",
|
"primeng": "^7.0.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
|
@ -152,9 +157,9 @@
|
||||||
"typescript": "3.1.6",
|
"typescript": "3.1.6",
|
||||||
"video.js": "^7",
|
"video.js": "^7",
|
||||||
"videojs-contextmenu-ui": "^5.0.0",
|
"videojs-contextmenu-ui": "^5.0.0",
|
||||||
|
"videojs-contrib-quality-levels": "^2.0.9",
|
||||||
"videojs-dock": "^2.0.2",
|
"videojs-dock": "^2.0.2",
|
||||||
"videojs-hotkeys": "^0.2.21",
|
"videojs-hotkeys": "^0.2.21",
|
||||||
"webpack": "^4.17.1",
|
|
||||||
"webpack-bundle-analyzer": "^3.0.2",
|
"webpack-bundle-analyzer": "^3.0.2",
|
||||||
"webpack-cli": "^3.0.8",
|
"webpack-cli": "^3.0.8",
|
||||||
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
|
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Component, OnInit, ViewChild } from '@angular/core'
|
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||||
import { Notifier, ServerService } from '@app/core'
|
import { Notifier, ServerService } from '@app/core'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
|
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
|
||||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-about-instance',
|
selector: 'my-about-instance',
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
|
<h4 i18n class="modal-title">Contact {{ instanceName }} administrator</h4>
|
||||||
<span class="close" aria-label="Close" role="button" (click)="hide()"></span>
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
|
@ -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 { Account } from '@app/shared/account/account.model'
|
||||||
import { AccountService } from '@app/shared/account/account.service'
|
import { AccountService } from '@app/shared/account/account.service'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-about',
|
selector: 'my-account-about',
|
||||||
|
|
|
@ -26,8 +26,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
private redirectService: RedirectService,
|
private redirectService: RedirectService,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
private i18n: I18n
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list.
|
||||||
import { JobsComponent } from './jobs/job.component'
|
import { JobsComponent } from './jobs/job.component'
|
||||||
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
||||||
import { JobService } from './jobs/shared/job.service'
|
import { JobService } from './jobs/shared/job.service'
|
||||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users'
|
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
|
||||||
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
|
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
|
||||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||||
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
||||||
|
@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
||||||
UsersComponent,
|
UsersComponent,
|
||||||
UserCreateComponent,
|
UserCreateComponent,
|
||||||
UserUpdateComponent,
|
UserUpdateComponent,
|
||||||
|
UserPasswordComponent,
|
||||||
UserListComponent,
|
UserListComponent,
|
||||||
|
|
||||||
ModerationComponent,
|
ModerationComponent,
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-expanded-text {
|
.moderation-expanded-text {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Moderation comment</h4>
|
<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>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -14,12 +15,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div i18n>
|
<div class="form-group" i18n>
|
||||||
This comment can only be seen by you or the other moderators.
|
This comment can only be seen by you or the other moderators.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group inputs">
|
<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
|
<input
|
||||||
type="submit" i18n-value value="Update this comment" class="action-button-submit"
|
type="submit" i18n-value value="Update this comment" class="action-button-submit"
|
||||||
|
@ -29,4 +30,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -45,7 +45,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
hideModerationCommentModal () {
|
hide () {
|
||||||
this.abuseToComment = undefined
|
this.abuseToComment = undefined
|
||||||
this.openedModal.close()
|
this.openedModal.close()
|
||||||
this.form.reset()
|
this.form.reset()
|
||||||
|
@ -60,7 +60,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
||||||
this.notifier.success(this.i18n('Comment updated.'))
|
this.notifier.success(this.i18n('Comment updated.'))
|
||||||
|
|
||||||
this.commentUpdated.emit(moderationComment)
|
this.commentUpdated.emit(moderationComment)
|
||||||
this.hideModerationCommentModal()
|
this.hide()
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -51,11 +51,11 @@
|
||||||
<td class="moderation-expanded" colspan="6">
|
<td class="moderation-expanded" colspan="6">
|
||||||
<div>
|
<div>
|
||||||
<span i18n class="moderation-expanded-label">Reason:</span>
|
<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>
|
||||||
<div *ngIf="videoAbuse.moderationComment">
|
<div *ngIf="videoAbuse.moderationComment">
|
||||||
<span i18n class="moderation-expanded-label">Moderation comment:</span>
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
|
||||||
import { ConfirmService } from '../../../core/index'
|
import { ConfirmService } from '../../../core/index'
|
||||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||||
import { Video } from '../../../shared/video/video.model'
|
import { Video } from '../../../shared/video/video.model'
|
||||||
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-abuse-list',
|
selector: 'my-video-abuse-list',
|
||||||
|
@ -30,7 +31,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private videoAbuseService: VideoAbuseService,
|
private videoAbuseService: VideoAbuseService,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private i18n: I18n
|
private i18n: I18n,
|
||||||
|
private markdownRenderer: MarkdownService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
|
@ -108,6 +110,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toHtml (text: string) {
|
||||||
|
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||||
|
}
|
||||||
|
|
||||||
protected loadData () {
|
protected loadData () {
|
||||||
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
|
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<th style="width: 40px"></th>
|
<th style="width: 40px"></th>
|
||||||
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
|
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
|
||||||
<th i18n>Sensitive</th>
|
<th i18n>Sensitive</th>
|
||||||
|
<th i18n>Unfederated</th>
|
||||||
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||||
<th style="width: 120px;"></th>
|
<th style="width: 120px;"></th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -26,7 +27,8 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>{{ videoBlacklist.video.nsfw }}</td>
|
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||||
|
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||||
<td>{{ videoBlacklist.createdAt }}</td>
|
<td>{{ videoBlacklist.createdAt }}</td>
|
||||||
|
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
|
@ -37,9 +39,9 @@
|
||||||
|
|
||||||
<ng-template pTemplate="rowexpansion" let-videoBlacklist>
|
<ng-template pTemplate="rowexpansion" let-videoBlacklist>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="moderation-expanded" colspan="5">
|
<td class="moderation-expanded" colspan="6">
|
||||||
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { VideoBlacklist } from '../../../../../../shared'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
|
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
|
||||||
import { Video } from '../../../shared/video/video.model'
|
import { Video } from '../../../shared/video/video.model'
|
||||||
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-blacklist-list',
|
selector: 'my-video-blacklist-list',
|
||||||
|
@ -26,6 +27,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private videoBlacklistService: VideoBlacklistService,
|
private videoBlacklistService: VideoBlacklistService,
|
||||||
|
private markdownRenderer: MarkdownService,
|
||||||
private i18n: I18n
|
private i18n: I18n
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
@ -46,6 +48,16 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
||||||
return Video.buildClientUrl(videoBlacklist.video.uuid)
|
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) {
|
async removeVideoFromBlacklist (entry: VideoBlacklist) {
|
||||||
const confirmMessage = this.i18n(
|
const confirmMessage = this.i18n(
|
||||||
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
|
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
export * from './user-create.component'
|
export * from './user-create.component'
|
||||||
export * from './user-update.component'
|
export * from './user-update.component'
|
||||||
|
export * from './user-password.component'
|
||||||
|
|
|
@ -81,3 +81,17 @@
|
||||||
|
|
||||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div *ngIf="!isCreation()" class="danger-zone">
|
||||||
|
<div class="account-title" i18n>Danger Zone</div>
|
||||||
|
|
||||||
|
<div class="form-group reset-password-email">
|
||||||
|
<label i18n>Send a link to reset the password by email to the user</label>
|
||||||
|
<button (click)="resetPassword()" i18n>Ask for new password</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label i18n>Manually set the user password</label>
|
||||||
|
<my-user-password [userId]="userId"></my-user-password>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ input:not([type=submit]) {
|
||||||
@include peertube-select-container(340px);
|
@include peertube-select-container(340px);
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=submit] {
|
input[type=submit], button {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include orange-button;
|
@include orange-button;
|
||||||
|
|
||||||
|
@ -25,3 +25,23 @@ input[type=submit] {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.account-title {
|
||||||
|
@include in-content-small-title;
|
||||||
|
|
||||||
|
margin-top: 55px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-zone {
|
||||||
|
.reset-password-email {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive {
|
||||||
videoQuotaDailyOptions: { value: string, label: string }[] = []
|
videoQuotaDailyOptions: { value: string, label: string }[] = []
|
||||||
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
|
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
|
||||||
username: string
|
username: string
|
||||||
|
userId: number
|
||||||
|
|
||||||
protected abstract serverService: ServerService
|
protected abstract serverService: ServerService
|
||||||
protected abstract configService: ConfigService
|
protected abstract configService: ConfigService
|
||||||
|
@ -22,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
|
||||||
}
|
}
|
||||||
|
|
||||||
computeQuotaWithTranscoding () {
|
computeQuotaWithTranscoding () {
|
||||||
const resolutions = this.serverService.getConfig().transcoding.enabledResolutions
|
const transcodingConfig = this.serverService.getConfig().transcoding
|
||||||
|
|
||||||
|
const resolutions = transcodingConfig.enabledResolutions
|
||||||
const higherResolution = VideoResolution.H_1080P
|
const higherResolution = VideoResolution.H_1080P
|
||||||
let multiplier = 0
|
let multiplier = 0
|
||||||
|
|
||||||
|
@ -30,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
|
||||||
multiplier += resolution / higherResolution
|
multiplier += resolution / higherResolution
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (transcodingConfig.hls.enabled) multiplier *= 2
|
||||||
|
|
||||||
return multiplier * parseInt(this.form.value['videoQuota'], 10)
|
return multiplier * parseInt(this.form.value['videoQuota'], 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetPassword () {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
protected buildQuotaOptions () {
|
protected buildQuotaOptions () {
|
||||||
// These are used by a HTML select, so convert key into strings
|
// These are used by a HTML select, so convert key into strings
|
||||||
this.videoQuotaOptions = this.configService
|
this.videoQuotaOptions = this.configService
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||||
|
<div class="form-group">
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="password" [attr.type]="showPassword ? 'text' : 'password'"
|
||||||
|
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||||
|
>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
|
||||||
|
<ng-container *ngIf="!showPassword" i18n>Show</ng-container>
|
||||||
|
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="formErrors.password" class="form-error">
|
||||||
|
{{ formErrors.password }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||||
|
</form>
|
|
@ -0,0 +1,22 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
input:not([type=submit]):not([type=checkbox]) {
|
||||||
|
@include peertube-input-text(340px);
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
@include peertube-button;
|
||||||
|
@include orange-button;
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-append {
|
||||||
|
height: 30px;
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Component, Input, OnInit } from '@angular/core'
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
|
import { UserService } from '@app/shared/users/user.service'
|
||||||
|
import { Notifier } from '../../../core'
|
||||||
|
import { User, UserUpdate } from '../../../../../../shared'
|
||||||
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||||
|
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
|
||||||
|
import { FormReactive } from '../../../shared'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-user-password',
|
||||||
|
templateUrl: './user-password.component.html',
|
||||||
|
styleUrls: [ './user-password.component.scss' ]
|
||||||
|
})
|
||||||
|
export class UserPasswordComponent extends FormReactive implements OnInit {
|
||||||
|
error: string
|
||||||
|
username: string
|
||||||
|
showPassword = false
|
||||||
|
|
||||||
|
@Input() userId: number
|
||||||
|
|
||||||
|
constructor (
|
||||||
|
protected formValidatorService: FormValidatorService,
|
||||||
|
private userValidatorsService: UserValidatorsService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private notifier: Notifier,
|
||||||
|
private userService: UserService,
|
||||||
|
private i18n: I18n
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
this.buildForm({
|
||||||
|
password: this.userValidatorsService.USER_PASSWORD
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
formValidated () {
|
||||||
|
this.error = undefined
|
||||||
|
|
||||||
|
const userUpdate: UserUpdate = this.form.value
|
||||||
|
|
||||||
|
this.userService.updateUser(this.userId, userUpdate).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(
|
||||||
|
this.i18n('Password changed for user {{username}}.', { username: this.username })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.error = err.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
togglePasswordVisibility () {
|
||||||
|
this.showPassword = !this.showPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
getFormButtonTitle () {
|
||||||
|
return this.i18n('Update user password')
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
|
||||||
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||||
error: string
|
error: string
|
||||||
userId: number
|
userId: number
|
||||||
|
userEmail: string
|
||||||
username: string
|
username: string
|
||||||
|
|
||||||
private paramsSub: Subscription
|
private paramsSub: Subscription
|
||||||
|
@ -89,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||||
return this.i18n('Update user')
|
return this.i18n('Update user')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetPassword () {
|
||||||
|
this.userService.askResetPassword(this.userEmail).subscribe(
|
||||||
|
() => {
|
||||||
|
this.notifier.success(
|
||||||
|
this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.error = err.message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private onUserFetched (userJson: User) {
|
private onUserFetched (userJson: User) {
|
||||||
this.userId = userJson.id
|
this.userId = userJson.id
|
||||||
this.username = userJson.username
|
this.username = userJson.username
|
||||||
|
this.userEmail = userJson.email
|
||||||
|
|
||||||
this.form.patchValue({
|
this.form.patchValue({
|
||||||
email: userJson.email,
|
email: userJson.email,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div i18n class="form-sub-title">Users list</div>
|
<div i18n class="form-sub-title">Users list</div>
|
||||||
|
|
||||||
<a class="add-button" routerLink="/admin/users/create">
|
<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>
|
<ng-container i18n>Create user</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,7 +65,9 @@
|
||||||
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
|
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
|
||||||
|
|
||||||
<ng-template #emailWithVerificationStatus>
|
<ng-template #emailWithVerificationStatus>
|
||||||
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
|
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
|
||||||
<em>? {{ user.email }}</em>
|
<em>? {{ user.email }}</em>
|
||||||
|
@ -76,6 +78,7 @@
|
||||||
</td>
|
</td>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
||||||
<td>{{ user.roleLabel }}</td>
|
<td>{{ user.roleLabel }}</td>
|
||||||
<td>{{ user.createdAt }}</td>
|
<td>{{ user.createdAt }}</td>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
.add-button {
|
.add-button {
|
||||||
@include create-button('../../../../assets/images/global/add.svg');
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.banned {
|
tr.banned {
|
||||||
|
@ -23,4 +23,4 @@ tr.banned {
|
||||||
input {
|
input {
|
||||||
@include peertube-input-text(250px);
|
@include peertube-input-text(250px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,10 +65,10 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #585858;
|
color: $grey-foreground-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #303030;
|
color: $grey-foreground-hover-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a routerLink="/my-account/settings" fragment="notifications" i18n>Notification preferences</a>
|
<a routerLink="/my-account/settings" fragment="notifications" i18n>
|
||||||
|
<my-global-icon iconName="cog"></my-global-icon>
|
||||||
|
Notification preferences
|
||||||
|
</a>
|
||||||
|
|
||||||
<button (click)="markAllAsRead()" i18n>Mark all as read</button>
|
<button (click)="markAllAsRead()" i18n>
|
||||||
|
<my-global-icon iconName="circle-tick"></my-global-icon>
|
||||||
|
Mark all as read
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-user-notifications #userNotification></my-user-notifications>
|
<my-user-notifications #userNotification></my-user-notifications>
|
||||||
|
|
|
@ -5,16 +5,18 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
@include grey-button;
|
@include grey-button;
|
||||||
|
@include button-with-icon(18px, 3px, -1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include grey-button;
|
@include grey-button;
|
||||||
|
@include button-with-icon(20px, 3px, -1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Accept ownership</h4>
|
<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>
|
||||||
|
|
||||||
<div class="modal-body" [formGroup]="form">
|
<div class="modal-body" [formGroup]="form">
|
||||||
|
|
|
@ -40,10 +40,10 @@
|
||||||
<td class="action-cell">
|
<td class="action-cell">
|
||||||
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
||||||
<my-button i18n label="Accept"
|
<my-button i18n label="Accept"
|
||||||
icon="icon-tick"
|
icon="tick"
|
||||||
(click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
(click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
||||||
<my-button i18n label="Refuse"
|
<my-button i18n label="Refuse"
|
||||||
icon="icon-cross"
|
icon="cross"
|
||||||
(click)="refuse(videoChangeOwnership)">Refuse</my-button>
|
(click)="refuse(videoChangeOwnership)">Refuse</my-button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="video-channels-header">
|
<div class="video-channels-header">
|
||||||
<a class="create-button" routerLink="create">
|
<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>
|
<ng-container i18n>Create another video channel</ng-container>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
.create-button {
|
.create-button {
|
||||||
@include create-button('../../../assets/images/global/add.svg');
|
@include create-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .action-button {
|
/deep/ .action-button {
|
||||||
|
|
|
@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
||||||
async deleteVideoChannel (videoChannel: VideoChannel) {
|
async deleteVideoChannel (videoChannel: VideoChannel) {
|
||||||
const res = await this.confirmService.confirmWithInput(
|
const res = await this.confirmService.confirmWithInput(
|
||||||
this.i18n(
|
this.i18n(
|
||||||
'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.',
|
'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' +
|
||||||
{ videoChannelName: videoChannel.displayName }
|
'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,
|
videoChannel.displayName,
|
||||||
this.i18n('Delete')
|
this.i18n('Delete')
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
<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>
|
<ng-container i18n>Delete</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
<my-button i18n-label label="Change ownership"
|
<my-button i18n-label label="Change ownership"
|
||||||
className="action-button-change-ownership"
|
className="action-button-change-ownership"
|
||||||
icon="icon-im-with-her"
|
icon="im-with-her"
|
||||||
(click)="changeOwnership($event, video)"
|
(click)="changeOwnership($event, video)"
|
||||||
></my-button>
|
></my-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -53,4 +53,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
|
<my-video-change-ownership #videoChangeOwnershipModal></my-video-change-ownership>
|
||||||
|
|
|
@ -23,14 +23,11 @@
|
||||||
.action-button-delete-selection {
|
.action-button-delete-selection {
|
||||||
@include peertube-button;
|
@include peertube-button;
|
||||||
@include orange-button;
|
@include orange-button;
|
||||||
}
|
@include button-with-icon(21px);
|
||||||
|
|
||||||
.icon.icon-delete-white {
|
my-global-icon {
|
||||||
@include icon(21px);
|
@include apply-svg-color(#fff);
|
||||||
|
}
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
background-image: url('../../../assets/images/global/delete-white.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Change ownership</h4>
|
<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>
|
||||||
|
|
||||||
<div class="modal-body" [formGroup]="form">
|
<div class="modal-body" [formGroup]="form">
|
||||||
|
|
|
@ -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 { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-channel-about',
|
selector: 'my-video-channel-about',
|
||||||
|
|
|
@ -30,14 +30,16 @@
|
||||||
|
|
||||||
<footer class="row">
|
<footer class="row">
|
||||||
<a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> -
|
<a href="https://joinpeertube.org" title="PeerTube website" target="_blank" rel="noopener noreferrer">PeerTube v{{ serverVersion }}{{ serverCommit }}</a> -
|
||||||
<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>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
|
<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
|
||||||
|
|
||||||
<my-confirm></my-confirm>
|
<my-confirm></my-confirm>
|
||||||
|
|
||||||
<p-toast position="bottom-right">
|
<p-toast position="bottom-right">
|
||||||
<ng-template let-message pTemplate="message">
|
<ng-template let-message pTemplate="message">
|
||||||
<div class="notification-block">
|
<div class="notification-block">
|
||||||
|
|
|
@ -3,12 +3,12 @@ import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
|
||||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { Notifier } from '@app/core/notification'
|
import { Notifier } from '@app/core/notification/notifier.service'
|
||||||
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
|
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken } from '../../../../../shared'
|
||||||
import { User } from '../../../../../shared/models/users'
|
import { User } from '../../../../../shared/models/users'
|
||||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||||
import { environment } from '../../../environments/environment'
|
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 { AuthStatus } from './auth-status.model'
|
||||||
import { AuthUser } from './auth-user.model'
|
import { AuthUser } from './auth-user.model'
|
||||||
import { objectToUrlEncoded } from '@app/shared/misc/utils'
|
import { objectToUrlEncoded } from '@app/shared/misc/utils'
|
||||||
|
|
|
@ -1,2 +1 @@
|
||||||
export * from './confirm.component'
|
|
||||||
export * from './confirm.service'
|
export * from './confirm.service'
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
||||||
import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
|
import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
|
||||||
|
|
||||||
import { AuthService } from './auth'
|
import { AuthService } from './auth'
|
||||||
import { ConfirmComponent, ConfirmService } from './confirm'
|
import { ConfirmService } from './confirm'
|
||||||
import { throwIfAlreadyLoaded } from './module-import-guard'
|
import { throwIfAlreadyLoaded } from './module-import-guard'
|
||||||
import { LoginGuard, RedirectService, UserRightGuard } from './routing'
|
import { LoginGuard, RedirectService, UserRightGuard } from './routing'
|
||||||
import { ServerService } from './server'
|
import { ServerService } from './server'
|
||||||
|
@ -18,6 +18,7 @@ import { CheatSheetComponent } from './hotkeys'
|
||||||
import { ToastModule } from 'primeng/toast'
|
import { ToastModule } from 'primeng/toast'
|
||||||
import { Notifier } from './notification'
|
import { Notifier } from './notification'
|
||||||
import { MessageService } from 'primeng/api'
|
import { MessageService } from 'primeng/api'
|
||||||
|
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -37,7 +38,6 @@ import { MessageService } from 'primeng/api'
|
||||||
],
|
],
|
||||||
|
|
||||||
declarations: [
|
declarations: [
|
||||||
ConfirmComponent,
|
|
||||||
CheatSheetComponent
|
CheatSheetComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -47,7 +47,6 @@ import { MessageService } from 'primeng/api'
|
||||||
|
|
||||||
ToastModule,
|
ToastModule,
|
||||||
|
|
||||||
ConfirmComponent,
|
|
||||||
CheatSheetComponent
|
CheatSheetComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -60,7 +59,8 @@ import { MessageService } from 'primeng/api'
|
||||||
UserRightGuard,
|
UserRightGuard,
|
||||||
RedirectService,
|
RedirectService,
|
||||||
Notifier,
|
Notifier,
|
||||||
MessageService
|
MessageService,
|
||||||
|
UserNotificationSocket
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CoreModule {
|
export class CoreModule {
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export * from './notifier.service'
|
export * from './notifier.service'
|
||||||
|
export * from './user-notification-socket.service'
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -51,7 +51,10 @@ export class ServerService {
|
||||||
requiresEmailVerification: false
|
requiresEmailVerification: false
|
||||||
},
|
},
|
||||||
transcoding: {
|
transcoding: {
|
||||||
enabledResolutions: []
|
enabledResolutions: [],
|
||||||
|
hls: {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
file: {
|
file: {
|
||||||
|
@ -87,6 +90,11 @@ export class ServerService {
|
||||||
enabled: false
|
enabled: false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
trending: {
|
||||||
|
videos: {
|
||||||
|
intervalDays: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private videoCategories: Array<VideoConstant<number>> = []
|
private videoCategories: Array<VideoConstant<number>> = []
|
||||||
|
|
|
@ -5,6 +5,6 @@
|
||||||
<span (click)="doSearch()" class="icon icon-search"></span>
|
<span (click)="doSearch()" class="icon icon-search"></span>
|
||||||
|
|
||||||
<a class="upload-button" routerLink="/videos/upload">
|
<a class="upload-button" routerLink="/videos/upload">
|
||||||
<span class="icon icon-upload"></span>
|
<my-global-icon iconName="upload"></my-global-icon>
|
||||||
<span i18n class="upload-button-label">Upload</span>
|
<span i18n class="upload-button-label">Upload</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
padding-right: 40px; // For the search icon
|
padding-right: 40px; // For the search icon
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--inputPlaceholderColor);
|
color: var(--inputPlaceholderColor);
|
||||||
|
@ -40,6 +41,7 @@
|
||||||
.upload-button {
|
.upload-button {
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
@include orange-button;
|
@include orange-button;
|
||||||
|
@include button-with-icon(22px, 3px, -1px);
|
||||||
|
|
||||||
margin-right: 25px;
|
margin-right: 25px;
|
||||||
|
|
||||||
|
@ -47,15 +49,6 @@
|
||||||
margin-right: 0;
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.icon-upload {
|
|
||||||
@include icon(22px);
|
|
||||||
|
|
||||||
background-image: url('../../assets/images/header/upload-white.svg');
|
|
||||||
height: 24px;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
|
|
@ -55,7 +55,8 @@
|
||||||
<ng-template #forgotPasswordModal>
|
<ng-template #forgotPasswordModal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Forgot your password</h4>
|
<h4 i18n class="modal-title">Forgot your password</h4>
|
||||||
<span class="close" aria-hidden="true" (click)="hideForgotPasswordModal()"></span>
|
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
></a>
|
></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false"></my-user-notifications>
|
<my-user-notifications [ignoreLoadingBar]="true" [infiniteScroll]="false" itemsPerPage="10"></my-user-notifications>
|
||||||
|
|
||||||
<a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a>
|
<a class="all-notifications" routerLink="/my-account/notifications" i18n>See all your notifications</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
/deep/ {
|
/deep/ {
|
||||||
.popover-notifications.popover {
|
.popover-notifications.popover {
|
||||||
max-width: 400px;
|
max-width: none;
|
||||||
|
|
||||||
.popover-body {
|
.popover-body {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -11,6 +11,7 @@
|
||||||
font-family: $main-fonts;
|
font-family: $main-fonts;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: 500px;
|
max-height: 500px;
|
||||||
|
width: 400px;
|
||||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
|
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
|
||||||
|
|
||||||
.notifications-header {
|
.notifications-header {
|
||||||
|
@ -40,7 +41,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
color: var(--mainForegroundColor);
|
color: var(--mainForegroundColor);
|
||||||
height: 30px;
|
padding: 7px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,7 +72,7 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
background-color: var(--mainColor);
|
background-color: var(--mainColor);
|
||||||
color: var(--mainBackgroundColor);
|
color: var(#fff);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
|
|
||||||
|
@ -80,3 +81,11 @@
|
||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-view) {
|
||||||
|
/deep/ {
|
||||||
|
.popover-notifications.popover .popover-body {
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||||
import { User } from '../shared/users/user.model'
|
import { User } from '../shared/users/user.model'
|
||||||
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
||||||
import { Subscription } from 'rxjs'
|
import { Subscription } from 'rxjs'
|
||||||
import { Notifier } from '@app/core'
|
import { Notifier, UserNotificationSocket } from '@app/core'
|
||||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NavigationEnd, Router } from '@angular/router'
|
import { NavigationEnd, Router } from '@angular/router'
|
||||||
import { filter } from 'rxjs/operators'
|
import { filter } from 'rxjs/operators'
|
||||||
|
@ -23,6 +23,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private userNotificationService: UserNotificationService,
|
private userNotificationService: UserNotificationService,
|
||||||
|
private userNotificationSocket: UserNotificationSocket,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
@ -53,7 +54,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private subscribeToNotifications () {
|
private subscribeToNotifications () {
|
||||||
this.notificationSub = this.userNotificationService.getMyNotificationsSocket()
|
this.notificationSub = this.userNotificationSocket.getMyNotificationsSocket()
|
||||||
.subscribe(data => {
|
.subscribe(data => {
|
||||||
if (data.type === 'new') return this.unreadNotifications++
|
if (data.type === 'new') return this.unreadNotifications++
|
||||||
if (data.type === 'read') return this.unreadNotifications--
|
if (data.type === 'read') return this.unreadNotifications--
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<ng-template #modal let-hide="close">
|
<ng-template #modal let-hide="close">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Change the language</h4>
|
<h4 i18n class="modal-title">Change the language</h4>
|
||||||
<span class="close" aria-label="Close" role="button" (click)="hide()"></span>
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ menu {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
color: var(--menuForegroundColor);
|
color: var(--menuForegroundColor);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -243,7 +243,7 @@ menu {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 400px) {
|
@media screen and (max-width: $mobile-view) {
|
||||||
.menu-wrapper {
|
.menu-wrapper {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="isVideo(result)" class="entry video">
|
<div *ngIf="isVideo(result)" class="entry video">
|
||||||
<my-video-thumbnail [video]="result"></my-video-thumbnail>
|
<my-video-thumbnail [video]="result" [nsfw]="isVideoBlur(result)"></my-video-thumbnail>
|
||||||
|
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a>
|
<a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', result.uuid]" [attr.title]="result.name">{{ result.name }}</a>
|
||||||
|
|
|
@ -87,10 +87,10 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #585858;
|
color: $grey-foreground-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #303030;
|
color: $grey-foreground-hover-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { AuthService, Notifier } from '@app/core'
|
import { AuthService, Notifier, ServerService } from '@app/core'
|
||||||
import { forkJoin, Subscription } from 'rxjs'
|
import { forkJoin, Subscription } from 'rxjs'
|
||||||
import { SearchService } from '@app/search/search.service'
|
import { SearchService } from '@app/search/search.service'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
|
@ -41,7 +41,8 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
private metaService: MetaService,
|
private metaService: MetaService,
|
||||||
private notifier: Notifier,
|
private notifier: Notifier,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private authService: AuthService
|
private authService: AuthService,
|
||||||
|
private serverService: ServerService
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
@ -75,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy {
|
||||||
if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
|
if (this.subActivatedRoute) this.subActivatedRoute.unsubscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isVideoBlur (video: Video) {
|
||||||
|
return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
|
||||||
|
}
|
||||||
|
|
||||||
isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
|
isVideoChannel (d: VideoChannel | Video): d is VideoChannel {
|
||||||
return d instanceof VideoChannel
|
return d instanceof VideoChannel
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer {
|
||||||
|
|
||||||
avatarUrl: string
|
avatarUrl: string
|
||||||
|
|
||||||
static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) {
|
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) {
|
||||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||||
|
|
||||||
if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path
|
if (actor && actor.avatar) return absoluteAPIUrl + actor.avatar.path
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
|
class="action-button" [ngClass]="{ small: buttonSize === 'small', grey: theme === 'grey', orange: theme === 'orange' }"
|
||||||
ngbDropdownToggle role="button"
|
ngbDropdownToggle role="button"
|
||||||
>
|
>
|
||||||
<span *ngIf="!label" class="icon icon-action"></span>
|
<my-global-icon *ngIf="!label" class="more-icon" iconName="more"></my-global-icon>
|
||||||
<span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
|
<span *ngIf="label" class="dropdown-toggle">{{ label }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -24,14 +24,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover, &:active, &:focus {
|
&:hover, &:active, &:focus {
|
||||||
background-color: $grey-color;
|
background-color: $grey-background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-action {
|
.more-icon {
|
||||||
@include icon(21px);
|
width: 21px;
|
||||||
|
|
||||||
background-image: url('../../../assets/images/video/more.svg');
|
|
||||||
top: -1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.small {
|
&.small {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<span class="action-button" [ngClass]="className" [title]="getTitle()">
|
<span class="action-button" [ngClass]="className" [title]="getTitle()">
|
||||||
<span class="icon" [ngClass]="icon"></span>
|
<my-global-icon [iconName]="icon"></my-global-icon>
|
||||||
<span class="button-label">{{ label }}</span>
|
<span class="button-label">{{ label }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -3,41 +3,18 @@
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
@include peertube-button-link;
|
@include peertube-button-link;
|
||||||
|
@include button-with-icon(21px, 0, -2px);
|
||||||
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
color: #585858;
|
color: $grey-foreground-color;
|
||||||
background-color: #E5E5E5;
|
background-color: $grey-background-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #EFEFEF;
|
background-color: $grey-background-hover-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
my-global-icon {
|
||||||
@include icon(21px);
|
@include apply-svg-color($grey-foreground-color);
|
||||||
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
|
|
||||||
&.icon-edit {
|
|
||||||
background-image: url('../../../assets/images/global/edit-grey.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-delete-grey {
|
|
||||||
background-image: url('../../../assets/images/global/delete-grey.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-im-with-her {
|
|
||||||
background-image: url('../../../assets/images/global/im-with-her.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-tick {
|
|
||||||
background-image: url('../../../assets/images/global/tick.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-cross {
|
|
||||||
background-image: url('../../../assets/images/global/cross.svg');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { GlobalIconName } from '@app/shared/icons/global-icon.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-button',
|
selector: 'my-button',
|
||||||
|
@ -9,7 +10,7 @@ import { Component, Input } from '@angular/core'
|
||||||
export class ButtonComponent {
|
export class ButtonComponent {
|
||||||
@Input() label = ''
|
@Input() label = ''
|
||||||
@Input() className: string = undefined
|
@Input() className: string = undefined
|
||||||
@Input() icon: string = undefined
|
@Input() icon: GlobalIconName = undefined
|
||||||
@Input() title: string = undefined
|
@Input() title: string = undefined
|
||||||
|
|
||||||
getTitle () {
|
getTitle () {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<span class="action-button action-button-delete" [title]="getTitle()" role="button">
|
<span class="action-button action-button-delete" [title]="getTitle()" role="button">
|
||||||
<span class="icon icon-delete-grey"></span>
|
<my-global-icon iconName="delete"></my-global-icon>
|
||||||
|
|
||||||
<span class="button-label" *ngIf="label">{{ label }}</span>
|
<span class="button-label" *ngIf="label">{{ label }}</span>
|
||||||
<span class="button-label" i18n *ngIf="!label">Delete</span>
|
<span class="button-label" i18n *ngIf="!label">Delete</span>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit">
|
<a class="action-button action-button-edit" [routerLink]="routerLink" i18n-title title="Edit">
|
||||||
<span class="icon icon-edit"></span>
|
<my-global-icon iconName="edit"></my-global-icon>
|
||||||
|
|
||||||
<span class="button-label" *ngIf="label">{{ label }}</span>
|
<span class="button-label" *ngIf="label">{{ label }}</span>
|
||||||
<span i18n class="button-label" *ngIf="!label">Edit</span>
|
<span i18n class="button-label" *ngIf="!label">Edit</span>
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title">{{ title }}</h4>
|
<h4 class="modal-title">{{ title }}</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>
|
||||||
|
|
||||||
<div class="modal-body" >
|
<div class="modal-body" >
|
|
@ -1,5 +1,5 @@
|
||||||
import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'
|
import { Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ConfirmService } from './confirm.service'
|
import { ConfirmService } from '@app/core/confirm/confirm.service'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
|
@ -10,20 +10,20 @@ export class VideoAbuseValidatorsService {
|
||||||
|
|
||||||
constructor (private i18n: I18n) {
|
constructor (private i18n: I18n) {
|
||||||
this.VIDEO_ABUSE_REASON = {
|
this.VIDEO_ABUSE_REASON = {
|
||||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
|
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||||
MESSAGES: {
|
MESSAGES: {
|
||||||
'required': this.i18n('Report reason is required.'),
|
'required': this.i18n('Report reason is required.'),
|
||||||
'minlength': this.i18n('Report reason must be at least 2 characters long.'),
|
'minlength': this.i18n('Report reason must be at least 2 characters long.'),
|
||||||
'maxlength': this.i18n('Report reason cannot be more than 300 characters long.')
|
'maxlength': this.i18n('Report reason cannot be more than 3000 characters long.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.VIDEO_ABUSE_MODERATION_COMMENT = {
|
this.VIDEO_ABUSE_MODERATION_COMMENT = {
|
||||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
|
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||||
MESSAGES: {
|
MESSAGES: {
|
||||||
'required': this.i18n('Moderation comment is required.'),
|
'required': this.i18n('Moderation comment is required.'),
|
||||||
'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
|
'minlength': this.i18n('Moderation comment must be at least 2 characters long.'),
|
||||||
'maxlength': this.i18n('Moderation comment cannot be more than 300 characters long.')
|
'maxlength': this.i18n('Moderation comment cannot be more than 3000 characters long.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
import { debounceTime, distinctUntilChanged } from 'rxjs/operators'
|
||||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import truncate from 'lodash-es/truncate'
|
import truncate from 'lodash-es/truncate'
|
||||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||||
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-markdown-textarea',
|
selector: 'my-markdown-textarea',
|
||||||
|
|
|
@ -53,6 +53,17 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const extension = '.' + file.name.split('.').pop()
|
||||||
|
if (this.extensions.includes(extension) === false) {
|
||||||
|
const message = this.i18n(
|
||||||
|
'PeerTube cannot handle this kind of file. Accepted extensions are {{extensions}}.',
|
||||||
|
{ extensions: this.allowedExtensionsMessage }
|
||||||
|
)
|
||||||
|
this.notifier.error(message)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.file = file
|
this.file = file
|
||||||
|
|
||||||
this.propagateChange(this.file)
|
this.propagateChange(this.file)
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
/deep/ svg {
|
||||||
|
width: inherit;
|
||||||
|
height: inherit;
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { Component, ElementRef, Input, OnInit } from '@angular/core'
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
'add': require('../../../assets/images/global/add.html'),
|
||||||
|
'syndication': require('../../../assets/images/global/syndication.html'),
|
||||||
|
'help': require('../../../assets/images/global/help.html'),
|
||||||
|
'sparkle': require('../../../assets/images/global/sparkle.html'),
|
||||||
|
'alert': require('../../../assets/images/global/alert.html'),
|
||||||
|
'cloud-error': require('../../../assets/images/global/cloud-error.html'),
|
||||||
|
'user-add': require('../../../assets/images/global/user-add.html'),
|
||||||
|
'no': require('../../../assets/images/global/no.html'),
|
||||||
|
'cloud-download': require('../../../assets/images/global/cloud-download.html'),
|
||||||
|
'undo': require('../../../assets/images/global/undo.html'),
|
||||||
|
'circle-tick': require('../../../assets/images/global/circle-tick.html'),
|
||||||
|
'cog': require('../../../assets/images/global/cog.html'),
|
||||||
|
'download': require('../../../assets/images/global/download.html'),
|
||||||
|
'edit': require('../../../assets/images/global/edit.html'),
|
||||||
|
'im-with-her': require('../../../assets/images/global/im-with-her.html'),
|
||||||
|
'delete': require('../../../assets/images/global/delete.html'),
|
||||||
|
'cross': require('../../../assets/images/global/cross.html'),
|
||||||
|
'validate': require('../../../assets/images/global/validate.html'),
|
||||||
|
'tick': require('../../../assets/images/global/tick.html'),
|
||||||
|
'dislike': require('../../../assets/images/video/dislike.html'),
|
||||||
|
'heart': require('../../../assets/images/video/heart.html'),
|
||||||
|
'like': require('../../../assets/images/video/like.html'),
|
||||||
|
'more': require('../../../assets/images/video/more.html'),
|
||||||
|
'share': require('../../../assets/images/video/share.html'),
|
||||||
|
'upload': require('../../../assets/images/video/upload.html')
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GlobalIconName = keyof typeof icons
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'my-global-icon',
|
||||||
|
template: '',
|
||||||
|
styleUrls: [ './global-icon.component.scss' ]
|
||||||
|
})
|
||||||
|
export class GlobalIconComponent implements OnInit {
|
||||||
|
@Input() iconName: GlobalIconName
|
||||||
|
|
||||||
|
constructor (private el: ElementRef) {}
|
||||||
|
|
||||||
|
ngOnInit () {
|
||||||
|
const nativeElement = this.el.nativeElement
|
||||||
|
|
||||||
|
nativeElement.innerHTML = icons[this.iconName]
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,4 +25,6 @@
|
||||||
[autoClose]="true"
|
[autoClose]="true"
|
||||||
(onHidden)="onPopoverHidden()"
|
(onHidden)="onPopoverHidden()"
|
||||||
(onShown)="onPopoverShown()"
|
(onShown)="onPopoverShown()"
|
||||||
></span>
|
>
|
||||||
|
<my-global-icon iconName="help"></my-global-icon>
|
||||||
|
</span>
|
||||||
|
|
|
@ -2,13 +2,17 @@
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
.help-tooltip-button {
|
.help-tooltip-button {
|
||||||
@include icon(17px);
|
cursor: pointer;
|
||||||
|
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
background-image: url('../../../assets/images/global/help.svg');
|
|
||||||
border: none;
|
border: none;
|
||||||
margin: 5px;
|
|
||||||
|
my-global-icon {
|
||||||
|
width: 17px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
margin: 5px;
|
||||||
|
|
||||||
|
@include apply-svg-color(var(--mainForegroundColor))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ {
|
/deep/ {
|
||||||
|
@ -16,16 +20,21 @@
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
|
|
||||||
.popover-body {
|
.popover-body {
|
||||||
|
font-family: $main-fonts;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-family: $main-fonts;
|
background-color: var(--mainBackgroundColor);
|
||||||
background-color: #fff;
|
color: var(--mainForegroundColor);
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component, Input, OnChanges, OnInit } from '@angular/core'
|
import { Component, Input, OnChanges, OnInit } from '@angular/core'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
import { MarkdownService } from '@app/shared/renderer'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-help',
|
selector: 'my-help',
|
||||||
|
|
|
@ -102,12 +102,18 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
|
||||||
return fd
|
return fd
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineFeedToHtml (obj: any, keyToNormalize: string) {
|
function objectLineFeedToHtml (obj: any, keyToNormalize: string) {
|
||||||
return immutableAssign(obj, {
|
return immutableAssign(obj, {
|
||||||
[keyToNormalize]: obj[keyToNormalize].replace(/\r?\n|\r/g, '<br />')
|
[keyToNormalize]: lineFeedToHtml(obj[keyToNormalize])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function lineFeedToHtml (text: string) {
|
||||||
|
if (!text) return text
|
||||||
|
|
||||||
|
return text.replace(/\r?\n|\r/g, '<br />')
|
||||||
|
}
|
||||||
|
|
||||||
function removeElementFromArray <T> (arr: T[], elem: T) {
|
function removeElementFromArray <T> (arr: T[], elem: T) {
|
||||||
const index = arr.indexOf(elem)
|
const index = arr.indexOf(elem)
|
||||||
if (index !== -1) arr.splice(index, 1)
|
if (index !== -1) arr.splice(index, 1)
|
||||||
|
@ -131,6 +137,7 @@ function scrollToTop () {
|
||||||
export {
|
export {
|
||||||
sortBy,
|
sortBy,
|
||||||
durationToString,
|
durationToString,
|
||||||
|
lineFeedToHtml,
|
||||||
objectToUrlEncoded,
|
objectToUrlEncoded,
|
||||||
getParameterByName,
|
getParameterByName,
|
||||||
populateAsyncUserVideoChannels,
|
populateAsyncUserVideoChannels,
|
||||||
|
@ -138,7 +145,7 @@ export {
|
||||||
dateToHuman,
|
dateToHuman,
|
||||||
immutableAssign,
|
immutableAssign,
|
||||||
objectToFormData,
|
objectToFormData,
|
||||||
lineFeedToHtml,
|
objectLineFeedToHtml,
|
||||||
removeElementFromArray,
|
removeElementFromArray,
|
||||||
scrollToTop
|
scrollToTop
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
<ng-template #modal>
|
<ng-template #modal>
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 i18n class="modal-title">Ban</h4>
|
<h4 i18n class="modal-title">Ban</h4>
|
||||||
<span class="close" aria-hidden="true" (click)="hideBanUserModal()"></span>
|
|
||||||
|
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
@ -19,7 +20,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group inputs">
|
<div class="form-group inputs">
|
||||||
<span i18n class="action-button action-button-cancel" (click)="hideBanUserModal()">Cancel</span>
|
<span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="submit" i18n-value value="Ban this user" class="action-button-submit"
|
type="submit" i18n-value value="Ban this user" class="action-button-submit"
|
||||||
|
@ -29,4 +30,4 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
|
||||||
this.openedModal = this.modalService.open(this.modal)
|
this.openedModal = this.modalService.open(this.modal)
|
||||||
}
|
}
|
||||||
|
|
||||||
hideBanUserModal () {
|
hide () {
|
||||||
this.usersToBan = undefined
|
this.usersToBan = undefined
|
||||||
this.openedModal.close()
|
this.openedModal.close()
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
|
||||||
this.notifier.success(message)
|
this.notifier.success(message)
|
||||||
|
|
||||||
this.userBanned.emit(this.usersToBan)
|
this.userBanned.emit(this.usersToBan)
|
||||||
this.hideBanUserModal()
|
this.hide()
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { LinkifierService } from '@app/shared/renderer/linkifier.service'
|
||||||
|
import * as sanitizeHtml from 'sanitize-html'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class HtmlRendererService {
|
||||||
|
|
||||||
|
constructor (private linkifier: LinkifierService) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
toSafeHtml (text: string) {
|
||||||
|
// Convert possible markdown to html
|
||||||
|
const html = this.linkifier.linkify(text)
|
||||||
|
|
||||||
|
return sanitizeHtml(html, {
|
||||||
|
allowedTags: [ 'a', 'p', 'span', 'br' ],
|
||||||
|
allowedSchemes: [ 'http', 'https' ],
|
||||||
|
allowedAttributes: {
|
||||||
|
'a': [ 'href', 'class', 'target' ]
|
||||||
|
},
|
||||||
|
transformTags: {
|
||||||
|
a: (tagName, attribs) => {
|
||||||
|
return {
|
||||||
|
tagName,
|
||||||
|
attribs: Object.assign(attribs, {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener noreferrer'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './html-renderer.service'
|
||||||
|
export * from './linkifier.service'
|
||||||
|
export * from './markdown.service'
|
|
@ -6,7 +6,6 @@ import { RouterModule } from '@angular/router'
|
||||||
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
|
import { MarkdownTextareaComponent } from '@app/shared/forms/markdown-textarea.component'
|
||||||
import { HelpComponent } from '@app/shared/misc/help.component'
|
import { HelpComponent } from '@app/shared/misc/help.component'
|
||||||
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
||||||
import { MarkdownService } from '@app/videos/shared'
|
|
||||||
|
|
||||||
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
||||||
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
import { SharedModule as PrimeSharedModule } from 'primeng/components/common/shared'
|
||||||
|
@ -34,10 +33,10 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||||
import {
|
import {
|
||||||
CustomConfigValidatorsService,
|
CustomConfigValidatorsService,
|
||||||
|
InstanceValidatorsService,
|
||||||
LoginValidatorsService,
|
LoginValidatorsService,
|
||||||
ReactiveFileComponent,
|
ReactiveFileComponent,
|
||||||
ResetPasswordValidatorsService,
|
ResetPasswordValidatorsService,
|
||||||
InstanceValidatorsService,
|
|
||||||
TextareaAutoResizeDirective,
|
TextareaAutoResizeDirective,
|
||||||
UserValidatorsService,
|
UserValidatorsService,
|
||||||
VideoAbuseValidatorsService,
|
VideoAbuseValidatorsService,
|
||||||
|
@ -67,6 +66,9 @@ import { UserHistoryService } from '@app/shared/users/user-history.service'
|
||||||
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
||||||
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
|
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
|
||||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
|
import { HtmlRendererService, LinkifierService, MarkdownService } from '@app/shared/renderer'
|
||||||
|
import { ConfirmComponent } from '@app/shared/confirm/confirm.component'
|
||||||
|
import { GlobalIconComponent } from '@app/shared/icons/global-icon.component'
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -110,7 +112,9 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
UserBanModalComponent,
|
UserBanModalComponent,
|
||||||
UserModerationDropdownComponent,
|
UserModerationDropdownComponent,
|
||||||
TopMenuDropdownComponent,
|
TopMenuDropdownComponent,
|
||||||
UserNotificationsComponent
|
UserNotificationsComponent,
|
||||||
|
ConfirmComponent,
|
||||||
|
GlobalIconComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -151,6 +155,8 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
UserModerationDropdownComponent,
|
UserModerationDropdownComponent,
|
||||||
TopMenuDropdownComponent,
|
TopMenuDropdownComponent,
|
||||||
UserNotificationsComponent,
|
UserNotificationsComponent,
|
||||||
|
ConfirmComponent,
|
||||||
|
GlobalIconComponent,
|
||||||
|
|
||||||
NumberFormatterPipe,
|
NumberFormatterPipe,
|
||||||
ObjectLengthPipe,
|
ObjectLengthPipe,
|
||||||
|
@ -167,7 +173,6 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
UserService,
|
UserService,
|
||||||
VideoService,
|
VideoService,
|
||||||
AccountService,
|
AccountService,
|
||||||
MarkdownService,
|
|
||||||
VideoChannelService,
|
VideoChannelService,
|
||||||
VideoCaptionService,
|
VideoCaptionService,
|
||||||
VideoImportService,
|
VideoImportService,
|
||||||
|
@ -192,6 +197,10 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
||||||
UserHistoryService,
|
UserHistoryService,
|
||||||
InstanceService,
|
InstanceService,
|
||||||
|
|
||||||
|
MarkdownService,
|
||||||
|
LinkifierService,
|
||||||
|
HtmlRendererService,
|
||||||
|
|
||||||
I18nPrimengCalendarService,
|
I18nPrimengCalendarService,
|
||||||
ScreenService,
|
ScreenService,
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo } from '../../../../../shared'
|
import { UserNotification as UserNotificationServer, UserNotificationType, VideoInfo, ActorInfo } from '../../../../../shared'
|
||||||
|
import { Actor } from '@app/shared/actor/actor.model'
|
||||||
|
|
||||||
export class UserNotification implements UserNotificationServer {
|
export class UserNotification implements UserNotificationServer {
|
||||||
id: number
|
id: number
|
||||||
|
@ -6,10 +7,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
read: boolean
|
read: boolean
|
||||||
|
|
||||||
video?: VideoInfo & {
|
video?: VideoInfo & {
|
||||||
channel: {
|
channel: ActorInfo & { avatarUrl?: string }
|
||||||
id: number
|
|
||||||
displayName: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
videoImport?: {
|
videoImport?: {
|
||||||
|
@ -23,10 +21,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
comment?: {
|
comment?: {
|
||||||
id: number
|
id: number
|
||||||
threadId: number
|
threadId: number
|
||||||
account: {
|
account: ActorInfo & { avatarUrl?: string }
|
||||||
id: number
|
|
||||||
displayName: string
|
|
||||||
}
|
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,18 +35,11 @@ export class UserNotification implements UserNotificationServer {
|
||||||
video: VideoInfo
|
video: VideoInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
account?: {
|
account?: ActorInfo & { avatarUrl?: string }
|
||||||
id: number
|
|
||||||
displayName: string
|
|
||||||
name: string
|
|
||||||
}
|
|
||||||
|
|
||||||
actorFollow?: {
|
actorFollow?: {
|
||||||
id: number
|
id: number
|
||||||
follower: {
|
follower: ActorInfo & { avatarUrl?: string }
|
||||||
name: string
|
|
||||||
displayName: string
|
|
||||||
}
|
|
||||||
following: {
|
following: {
|
||||||
type: 'account' | 'channel'
|
type: 'account' | 'channel'
|
||||||
name: string
|
name: string
|
||||||
|
@ -76,12 +64,22 @@ export class UserNotification implements UserNotificationServer {
|
||||||
this.read = hash.read
|
this.read = hash.read
|
||||||
|
|
||||||
this.video = hash.video
|
this.video = hash.video
|
||||||
|
if (this.video) this.setAvatarUrl(this.video.channel)
|
||||||
|
|
||||||
this.videoImport = hash.videoImport
|
this.videoImport = hash.videoImport
|
||||||
|
|
||||||
this.comment = hash.comment
|
this.comment = hash.comment
|
||||||
|
if (this.comment) this.setAvatarUrl(this.comment.account)
|
||||||
|
|
||||||
this.videoAbuse = hash.videoAbuse
|
this.videoAbuse = hash.videoAbuse
|
||||||
|
|
||||||
this.videoBlacklist = hash.videoBlacklist
|
this.videoBlacklist = hash.videoBlacklist
|
||||||
|
|
||||||
this.account = hash.account
|
this.account = hash.account
|
||||||
|
if (this.account) this.setAvatarUrl(this.account)
|
||||||
|
|
||||||
this.actorFollow = hash.actorFollow
|
this.actorFollow = hash.actorFollow
|
||||||
|
if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower)
|
||||||
|
|
||||||
this.createdAt = hash.createdAt
|
this.createdAt = hash.createdAt
|
||||||
this.updatedAt = hash.updatedAt
|
this.updatedAt = hash.updatedAt
|
||||||
|
@ -97,6 +95,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
|
|
||||||
case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
|
case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
|
||||||
case UserNotificationType.COMMENT_MENTION:
|
case UserNotificationType.COMMENT_MENTION:
|
||||||
|
this.accountUrl = this.buildAccountUrl(this.comment.account)
|
||||||
this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
|
this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -138,8 +137,8 @@ export class UserNotification implements UserNotificationServer {
|
||||||
return '/videos/watch/' + video.uuid
|
return '/videos/watch/' + video.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildAccountUrl (account: { name: string }) {
|
private buildAccountUrl (account: { name: string, host: string }) {
|
||||||
return '/accounts/' + account.name
|
return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildVideoImportUrl () {
|
private buildVideoImportUrl () {
|
||||||
|
@ -150,4 +149,7 @@ export class UserNotification implements UserNotificationServer {
|
||||||
return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
|
return videoImport.targetUrl || videoImport.magnetUri || videoImport.torrentName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setAvatarUrl (actor: { avatarUrl?: string, avatar?: { path: string } }) {
|
||||||
|
actor.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(actor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,26 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { RestExtractor, RestService } from '@app/shared/rest'
|
import { RestExtractor, RestService } from '../rest'
|
||||||
import { catchError, map, tap } from 'rxjs/operators'
|
import { catchError, map, tap } from 'rxjs/operators'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
|
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
|
||||||
import { UserNotification } from '@app/shared/users/user-notification.model'
|
import { UserNotification } from './user-notification.model'
|
||||||
import { Subject } from 'rxjs'
|
import { AuthService } from '../../core'
|
||||||
import * as io from 'socket.io-client'
|
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||||
import { AuthService } from '@app/core'
|
import { User } from '..'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
|
||||||
import { User } from '@app/shared'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserNotificationService {
|
export class UserNotificationService {
|
||||||
static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
|
static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
|
||||||
static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
|
static BASE_NOTIFICATION_SETTINGS = environment.apiUrl + '/api/v1/users/me/notification-settings'
|
||||||
|
|
||||||
private notificationSubject = new Subject<{ type: 'new' | 'read' | 'read-all', notification?: UserNotification }>()
|
|
||||||
|
|
||||||
private socket: SocketIOClient.Socket
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private authHttp: HttpClient,
|
private authHttp: HttpClient,
|
||||||
private restExtractor: RestExtractor,
|
private restExtractor: RestExtractor,
|
||||||
private restService: RestService
|
private restService: RestService,
|
||||||
|
private userNotificationSocket: UserNotificationSocket
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
|
listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
|
||||||
|
@ -48,16 +44,6 @@ export class UserNotificationService {
|
||||||
.pipe(map(n => n.total))
|
.pipe(map(n => n.total))
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyNotificationsSocket () {
|
|
||||||
const socket = this.getSocket()
|
|
||||||
|
|
||||||
socket.on('new-notification', (n: UserNotificationServer) => {
|
|
||||||
this.notificationSubject.next({ type: 'new', notification: new UserNotification(n) })
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.notificationSubject.asObservable()
|
|
||||||
}
|
|
||||||
|
|
||||||
markAsRead (notification: UserNotification) {
|
markAsRead (notification: UserNotification) {
|
||||||
const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
|
const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
|
||||||
|
|
||||||
|
@ -67,7 +53,7 @@ export class UserNotificationService {
|
||||||
return this.authHttp.post(url, body, { headers })
|
return this.authHttp.post(url, body, { headers })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
tap(() => this.notificationSubject.next({ type: 'read' })),
|
tap(() => this.userNotificationSocket.dispatch('read')),
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -79,7 +65,7 @@ export class UserNotificationService {
|
||||||
return this.authHttp.post(url, {}, { headers })
|
return this.authHttp.post(url, {}, { headers })
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
tap(() => this.notificationSubject.next({ type: 'read-all' })),
|
tap(() => this.userNotificationSocket.dispatch('read-all')),
|
||||||
catchError(res => this.restExtractor.handleError(res))
|
catchError(res => this.restExtractor.handleError(res))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -94,16 +80,6 @@ export class UserNotificationService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSocket () {
|
|
||||||
if (this.socket) return this.socket
|
|
||||||
|
|
||||||
this.socket = io(environment.apiUrl + '/user-notifications', {
|
|
||||||
query: { accessToken: this.auth.getAccessToken() }
|
|
||||||
})
|
|
||||||
|
|
||||||
return this.socket
|
|
||||||
}
|
|
||||||
|
|
||||||
private formatNotification (notification: UserNotificationServer) {
|
private formatNotification (notification: UserNotificationServer) {
|
||||||
return new UserNotification(notification)
|
return new UserNotification(notification)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,61 +1,101 @@
|
||||||
<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
|
<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
|
||||||
|
|
||||||
<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
|
<div class="notifications" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()">
|
||||||
<div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }">
|
<div *ngFor="let notification of notifications" class="notification" [ngClass]="{ unread: !notification.read }" (click)="markAsRead(notification)">
|
||||||
|
|
||||||
<div [ngSwitch]="notification.type">
|
<ng-container [ngSwitch]="notification.type">
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_FROM_SUBSCRIPTION">
|
||||||
{{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
|
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.video.channel.avatarUrl" />
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
{{ notification.video.channel.displayName }} published a <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">new video</a>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
|
||||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted
|
<my-global-icon iconName="undo"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been unblacklisted
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
|
||||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted
|
<my-global-icon iconName="no"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoBlacklist.video.name }}</a> has been blacklisted
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
|
<my-global-icon iconName="alert"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.videoAbuseUrl">A new video abuse</a> has been created on video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.videoAbuse.video.name }}</a>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_COMMENT_ON_MY_VIDEO">
|
||||||
{{ notification.comment.account.displayName }} commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
|
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> commented your video <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">{{ notification.comment.video.name }}</a>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_PUBLISHED">
|
||||||
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
|
<my-global-icon iconName="sparkle"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
Your video <a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">{{ notification.video.name }}</a> has been published
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
|
<my-global-icon iconName="cloud-download"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.videoUrl">Your video import</a> {{ notification.videoImportIdentifier }} succeeded
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
|
<my-global-icon iconName="cloud-error"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<a (click)="markAsRead(notification)" [routerLink]="notification.videoImportUrl">Your video import</a> {{ notification.videoImportIdentifier }} failed
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
|
||||||
User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance
|
<my-global-icon iconName="user-add"></my-global-icon>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
User <a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.account.name }} registered</a> on your instance
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
|
||||||
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
|
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
|
||||||
|
|
||||||
<ng-container *ngIf="notification.actorFollow.following.type === 'channel'">
|
<div class="message">
|
||||||
your channel {{ notification.actorFollow.following.displayName }}
|
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
|
<ng-container *ngIf="notification.actorFollow.following.type === 'channel'">your channel {{ notification.actorFollow.following.displayName }}</ng-container>
|
||||||
|
<ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
|
<ng-container i18n *ngSwitchCase="UserNotificationType.COMMENT_MENTION">
|
||||||
{{ notification.comment.account.displayName }} mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
|
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div i18n title="Mark as read" class="mark-as-read">
|
<div class="message">
|
||||||
<div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div>
|
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.comment.account.displayName }}</a> mentioned you on <a (click)="markAsRead(notification)" [routerLink]="notification.commentUrl">video {{ notification.comment.video.name }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<div class="from-date">{{ notification.createdAt | myFromNow }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,30 +1,51 @@
|
||||||
|
@import '_variables';
|
||||||
|
@import '_mixins';
|
||||||
|
|
||||||
|
.no-notification {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.notification {
|
.notification {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
padding: 15px 10px;
|
padding: 15px 5px 15px 10px;
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.10);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.10);
|
||||||
|
|
||||||
.mark-as-read {
|
|
||||||
min-width: 35px;
|
|
||||||
|
|
||||||
.glyphicon {
|
|
||||||
display: none;
|
|
||||||
cursor: pointer;
|
|
||||||
color: rgba(20, 20, 20, 0.5)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unread {
|
&.unread {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
&:hover .mark-as-read .glyphicon {
|
my-global-icon {
|
||||||
display: block;
|
width: 24px;
|
||||||
|
margin-right: 11px;
|
||||||
|
margin-left: 3px;
|
||||||
|
|
||||||
&:hover {
|
@include apply-svg-color(#333);
|
||||||
color: rgba(20, 20, 20, 0.8);
|
}
|
||||||
}
|
|
||||||
|
.avatar {
|
||||||
|
@include avatar(30px);
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: $font-semibold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.from-date {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: $grey-foreground-color;
|
||||||
|
padding-left: 5px;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,17 +13,14 @@ import { UserNotification } from '@app/shared/users/user-notification.model'
|
||||||
export class UserNotificationsComponent implements OnInit {
|
export class UserNotificationsComponent implements OnInit {
|
||||||
@Input() ignoreLoadingBar = false
|
@Input() ignoreLoadingBar = false
|
||||||
@Input() infiniteScroll = true
|
@Input() infiniteScroll = true
|
||||||
|
@Input() itemsPerPage = 20
|
||||||
|
|
||||||
notifications: UserNotification[] = []
|
notifications: UserNotification[] = []
|
||||||
|
|
||||||
// So we can access it in the template
|
// So we can access it in the template
|
||||||
UserNotificationType = UserNotificationType
|
UserNotificationType = UserNotificationType
|
||||||
|
|
||||||
componentPagination: ComponentPagination = {
|
componentPagination: ComponentPagination
|
||||||
currentPage: 1,
|
|
||||||
itemsPerPage: 10,
|
|
||||||
totalItems: null
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private userNotificationService: UserNotificationService,
|
private userNotificationService: UserNotificationService,
|
||||||
|
@ -31,6 +28,12 @@ export class UserNotificationsComponent implements OnInit {
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
ngOnInit () {
|
ngOnInit () {
|
||||||
|
this.componentPagination = {
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable
|
||||||
|
totalItems: null
|
||||||
|
}
|
||||||
|
|
||||||
this.loadMoreNotifications()
|
this.loadMoreNotifications()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +60,8 @@ export class UserNotificationsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
markAsRead (notification: UserNotification) {
|
markAsRead (notification: UserNotification) {
|
||||||
|
if (notification.read) return
|
||||||
|
|
||||||
this.userNotificationService.markAsRead(notification)
|
this.userNotificationService.markAsRead(notification)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
|
|
|
@ -32,9 +32,7 @@ export class VideoAbuseService {
|
||||||
|
|
||||||
reportVideo (id: number, reason: string) {
|
reportVideo (id: number, reason: string) {
|
||||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
|
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
|
||||||
const body = {
|
const body = { reason }
|
||||||
reason
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.authHttp.post(url, body)
|
return this.authHttp.post(url, body)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -36,8 +36,11 @@ export class VideoBlacklistService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
blacklistVideo (videoId: number, reason?: string) {
|
blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
|
||||||
const body = reason ? { reason } : {}
|
const body = {
|
||||||
|
unfederate,
|
||||||
|
reason
|
||||||
|
}
|
||||||
|
|
||||||
return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
|
return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -82,6 +82,7 @@ export class VideoImportService {
|
||||||
nsfw: video.nsfw,
|
nsfw: video.nsfw,
|
||||||
waitTranscoding: video.waitTranscoding,
|
waitTranscoding: video.waitTranscoding,
|
||||||
commentsEnabled: video.commentsEnabled,
|
commentsEnabled: video.commentsEnabled,
|
||||||
|
downloadEnabled: video.downloadEnabled,
|
||||||
thumbnailfile: video.thumbnailfile,
|
thumbnailfile: video.thumbnailfile,
|
||||||
previewfile: video.previewfile,
|
previewfile: video.previewfile,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
<div [ngClass]="{ 'margin-content': marginContent }">
|
<div [ngClass]="{ 'margin-content': marginContent }">
|
||||||
<div class="videos-header">
|
<div class="videos-header">
|
||||||
<div *ngIf="titlePage" class="title-page title-page-single">
|
<div *ngIf="titlePage" class="title-page title-page-single">
|
||||||
{{ titlePage }}
|
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
|
||||||
|
{{ titlePage }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-feed [syndicationItems]="syndicationItems"></my-feed>
|
<my-feed [syndicationItems]="syndicationItems"></my-feed>
|
||||||
|
|
||||||
<div class="moderation-block" *ngIf="displayModerationBlock">
|
<div class="moderation-block" *ngIf="displayModerationBlock">
|
||||||
|
|
|
@ -19,8 +19,8 @@
|
||||||
|
|
||||||
my-feed {
|
my-feed {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
|
||||||
top: 1px;
|
top: 1px;
|
||||||
|
min-width: 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.moderation-block {
|
.moderation-block {
|
||||||
|
|
|
@ -39,6 +39,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
||||||
ownerDisplayType: OwnerDisplayType = 'account'
|
ownerDisplayType: OwnerDisplayType = 'account'
|
||||||
firstLoadedPage: number
|
firstLoadedPage: number
|
||||||
displayModerationBlock = false
|
displayModerationBlock = false
|
||||||
|
titleTooltip: string
|
||||||
|
|
||||||
protected baseVideoWidth = 215
|
protected baseVideoWidth = 215
|
||||||
protected baseVideoHeight = 205
|
protected baseVideoHeight = 205
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<div class="video-feed">
|
<div class="video-feed">
|
||||||
<span
|
<my-global-icon
|
||||||
*ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom"
|
*ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom"
|
||||||
class="icon icon-syndication" role="button"
|
class="icon-syndication" role="button" iconName="syndication"
|
||||||
></span>
|
>
|
||||||
|
</my-global-icon>
|
||||||
|
|
||||||
<ng-template #feedsList>
|
<ng-template #feedsList>
|
||||||
<a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
|
<a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
@import '_variables';
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
|
|
||||||
.video-feed {
|
.video-feed {
|
||||||
|
@ -6,14 +7,12 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
my-global-icon {
|
||||||
@include icon(12px);
|
cursor: pointer;
|
||||||
|
width: 12px;
|
||||||
|
position: relative;
|
||||||
|
top: -2px;
|
||||||
|
|
||||||
&.icon-syndication {
|
@include apply-svg-color(var(--mainForegroundColor))
|
||||||
position: relative;
|
|
||||||
top: -2px;
|
|
||||||
background-color: var(--mainForegroundColor);
|
|
||||||
mask-image: url('../../../assets/images/global/syndication.svg');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import { AuthUser } from '../../core'
|
||||||
import { Video } from '../../shared/video/video.model'
|
import { Video } from '../../shared/video/video.model'
|
||||||
import { Account } from '@app/shared/account/account.model'
|
import { Account } from '@app/shared/account/account.model'
|
||||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||||
|
import { VideoStreamingPlaylist } from '../../../../../shared/models/videos/video-streaming-playlist.model'
|
||||||
|
import { VideoStreamingPlaylistType } from '../../../../../shared/models/videos/video-streaming-playlist.type'
|
||||||
|
|
||||||
export class VideoDetails extends Video implements VideoDetailsServerModel {
|
export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
descriptionPath: string
|
descriptionPath: string
|
||||||
|
@ -12,6 +14,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
files: VideoFile[]
|
files: VideoFile[]
|
||||||
account: Account
|
account: Account
|
||||||
commentsEnabled: boolean
|
commentsEnabled: boolean
|
||||||
|
downloadEnabled: boolean
|
||||||
|
|
||||||
waitTranscoding: boolean
|
waitTranscoding: boolean
|
||||||
state: VideoConstant<VideoState>
|
state: VideoConstant<VideoState>
|
||||||
|
@ -19,6 +22,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
likesPercent: number
|
likesPercent: number
|
||||||
dislikesPercent: number
|
dislikesPercent: number
|
||||||
|
|
||||||
|
trackerUrls: string[]
|
||||||
|
|
||||||
|
streamingPlaylists: VideoStreamingPlaylist[]
|
||||||
|
|
||||||
constructor (hash: VideoDetailsServerModel, translations = {}) {
|
constructor (hash: VideoDetailsServerModel, translations = {}) {
|
||||||
super(hash, translations)
|
super(hash, translations)
|
||||||
|
|
||||||
|
@ -29,6 +36,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
this.tags = hash.tags
|
this.tags = hash.tags
|
||||||
this.support = hash.support
|
this.support = hash.support
|
||||||
this.commentsEnabled = hash.commentsEnabled
|
this.commentsEnabled = hash.commentsEnabled
|
||||||
|
this.downloadEnabled = hash.downloadEnabled
|
||||||
|
|
||||||
|
this.trackerUrls = hash.trackerUrls
|
||||||
|
this.streamingPlaylists = hash.streamingPlaylists
|
||||||
|
|
||||||
this.buildLikeAndDislikePercents()
|
this.buildLikeAndDislikePercents()
|
||||||
}
|
}
|
||||||
|
@ -53,4 +64,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||||
this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
|
this.likesPercent = (this.likes / (this.likes + this.dislikes)) * 100
|
||||||
this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
|
this.dislikesPercent = (this.dislikes / (this.likes + this.dislikes)) * 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getHlsPlaylist () {
|
||||||
|
return this.streamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ export class VideoEdit implements VideoUpdate {
|
||||||
tags: string[]
|
tags: string[]
|
||||||
nsfw: boolean
|
nsfw: boolean
|
||||||
commentsEnabled: boolean
|
commentsEnabled: boolean
|
||||||
|
downloadEnabled: boolean
|
||||||
waitTranscoding: boolean
|
waitTranscoding: boolean
|
||||||
channelId: number
|
channelId: number
|
||||||
privacy: VideoPrivacy
|
privacy: VideoPrivacy
|
||||||
|
@ -27,7 +28,15 @@ export class VideoEdit implements VideoUpdate {
|
||||||
scheduleUpdate?: VideoScheduleUpdate
|
scheduleUpdate?: VideoScheduleUpdate
|
||||||
originallyPublishedAt?: Date | string
|
originallyPublishedAt?: Date | string
|
||||||
|
|
||||||
constructor (video?: Video & { tags: string[], commentsEnabled: boolean, support: string, thumbnailUrl: string, previewUrl: string }) {
|
constructor (
|
||||||
|
video?: Video & {
|
||||||
|
tags: string[],
|
||||||
|
commentsEnabled: boolean,
|
||||||
|
downloadEnabled: boolean,
|
||||||
|
support: string,
|
||||||
|
thumbnailUrl: string,
|
||||||
|
previewUrl: string
|
||||||
|
}) {
|
||||||
if (video) {
|
if (video) {
|
||||||
this.id = video.id
|
this.id = video.id
|
||||||
this.uuid = video.uuid
|
this.uuid = video.uuid
|
||||||
|
@ -39,6 +48,7 @@ export class VideoEdit implements VideoUpdate {
|
||||||
this.tags = video.tags
|
this.tags = video.tags
|
||||||
this.nsfw = video.nsfw
|
this.nsfw = video.nsfw
|
||||||
this.commentsEnabled = video.commentsEnabled
|
this.commentsEnabled = video.commentsEnabled
|
||||||
|
this.downloadEnabled = video.downloadEnabled
|
||||||
this.waitTranscoding = video.waitTranscoding
|
this.waitTranscoding = video.waitTranscoding
|
||||||
this.channelId = video.channel.id
|
this.channelId = video.channel.id
|
||||||
this.privacy = video.privacy.id
|
this.privacy = video.privacy.id
|
||||||
|
@ -88,6 +98,7 @@ export class VideoEdit implements VideoUpdate {
|
||||||
tags: this.tags,
|
tags: this.tags,
|
||||||
nsfw: this.nsfw,
|
nsfw: this.nsfw,
|
||||||
commentsEnabled: this.commentsEnabled,
|
commentsEnabled: this.commentsEnabled,
|
||||||
|
downloadEnabled: this.downloadEnabled,
|
||||||
waitTranscoding: this.waitTranscoding,
|
waitTranscoding: this.waitTranscoding,
|
||||||
channelId: this.channelId,
|
channelId: this.channelId,
|
||||||
privacy: this.privacy,
|
privacy: this.privacy,
|
||||||
|
|
|
@ -50,10 +50,10 @@
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #585858;
|
color: $grey-foreground-color;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #303030;
|
color: $grey-foreground-hover-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ export class Video implements VideoServerModel {
|
||||||
displayName: string
|
displayName: string
|
||||||
url: string
|
url: string
|
||||||
host: string
|
host: string
|
||||||
avatar: Avatar
|
avatar?: Avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
channel: {
|
channel: {
|
||||||
|
@ -64,7 +64,7 @@ export class Video implements VideoServerModel {
|
||||||
displayName: string
|
displayName: string
|
||||||
url: string
|
url: string
|
||||||
host: string
|
host: string
|
||||||
avatar: Avatar
|
avatar?: Avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
userHistory?: {
|
userHistory?: {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue