Merge branch 'develop' into pr/1285
This commit is contained in:
commit
b718fd2237
89
CHANGELOG.md
89
CHANGELOG.md
|
@ -1,10 +1,99 @@
|
|||
# Changelog
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
* **Docker:** Check you have all the storage fields in your `/config/production.yaml` file: https://github.com/Chocobozzz/PeerTube/blob/develop/support/docker/production/config/production.yaml#L34
|
||||
* **nginx:** Add redundancy endpoint in static file. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx**
|
||||
* **nginx:** Add socket io endpoint. **You should add it in your nginx configuration: https://github.com/Chocobozzz/PeerTube/blob/develop/support/doc/production.md#nginx**
|
||||
* Moderators can manage users now (add/delete/update/block)
|
||||
* Add `tmp` and `redundancy` directories in configuration file. **You should configure them in your production.yaml**
|
||||
|
||||
### Maintenance
|
||||
|
||||
* Check free storage before upgrading in upgrade script ([@Nutomic](https://github.com/nutomic))
|
||||
* Explain that PeerTube must be stopped in prune storage script
|
||||
* Add some security directives in the systemd unit configuration file ([@rigelk](https://github.com/rigelk) & [@mkoppmann](https://github.com/mkoppmann))
|
||||
* Update FreeBSD startup script ([@gegeweb](https://github.com/gegeweb))
|
||||
|
||||
### Docker
|
||||
|
||||
* Patch docker entrypoint to speed up the chown at startup ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
|
||||
### Features
|
||||
|
||||
* Add Russian, Polish and Italian languages
|
||||
* Add user notifications:
|
||||
* Notification types:
|
||||
* Comment on my video
|
||||
* New video from my subscriptions
|
||||
* New video abuses (for moderators)
|
||||
* Blacklist/Unblacklist on my video
|
||||
* Video import finished (error or success)
|
||||
* Pending video published (after transcoding or a scheduled update)
|
||||
* My account or one of my channel has a new follower
|
||||
* Someone (except muted accounts) mentioned me in comments
|
||||
* A user registered on the instance (for moderators)
|
||||
* Notification actions:
|
||||
* Add a web notification
|
||||
* Send an english email
|
||||
* Add contact form in about page (**enabled by default**)
|
||||
* Add ability to unfederate a local video in blacklist modal (**checkbox checked by default**)
|
||||
* Support additional video extensions if transcoding is enabled (**enabled by default**)
|
||||
* Redirect to the last url on login
|
||||
* Add ability to automatically set the video caption in URL. Example: https://peertube2.cpy.re/videos/watch/9c9de5e8-0a1e-484a-b099-e80766180a6d?subtitle=ru
|
||||
* Automatically enable the last selected caption when watching a video
|
||||
* Add ability to disable, clear and list user videos history
|
||||
* Add a button to help to translate peertube
|
||||
* Add text in the report modal to explain to whom the report will be sent
|
||||
* Open my account menu entries on hover
|
||||
* Explain what features are enabled on the instance in the about page
|
||||
* Add an error message in the forgot password modal if the instance email system is not configured
|
||||
* Add sitemap
|
||||
* Add well known url to change password ([@rigelk](https://github.com/rigelk))
|
||||
* Remove 8GB video upload limit on client side. There may still be such limit depending on the reverse proxy configuration ([@scanlime](https://github.com/scanlime))
|
||||
* Add CSP ([@rigelk](https://github.com/rigelk) & [@Nutomic](https://github.com/nutomic))
|
||||
* Update title and description HTML tags when rendering video HTML page
|
||||
* Add webfinger support for remote follows ([@acid-chicken](https://github.com/acid-chicken))
|
||||
* Add tooltip to explain how the trending algorithm works ([@auberanger](https://github.com/auberanger))
|
||||
* Warn users when they want to delete a channel because they will not be able to create another channel with the same name
|
||||
* Warn users when they leave the video upload/update (on page refresh/tab close)
|
||||
* Set max user name, user display name, channel name and channel display name lengths to 50 characters ([@McFlat](https://github.com/mcflat))
|
||||
* Increase video abuse length to 3000 characters
|
||||
* Add totalLocalVideoFilesSize in the stats endpoint
|
||||
|
||||
## Bug fixes
|
||||
|
||||
* Fix the addition of captions to a video
|
||||
* Fix federation of some videos
|
||||
* Fix NSFW blur on search
|
||||
* Add error message when trying to upload .ass subtitles
|
||||
* Fix default homepage in the progressive web application
|
||||
* Don't crash on queue error
|
||||
* Fix EXDEV errors if you have multiple mount points
|
||||
* Fix broken audio in transcoding with some videos
|
||||
* Fix crash on getVideoFileStream issue
|
||||
* Fix followers search
|
||||
* Remove trailing `/` in CLI import script ([@HesioZ](https://github.com/HesioZ/))
|
||||
* Use origin video url in canonical tag
|
||||
* Fix captions in HTTP fallback
|
||||
* Automatically refresh remote actors to fix deleted remote actors that are still displayed on some instances
|
||||
* Add missing translations in video embed page
|
||||
* Fix some styling issues in dark mode
|
||||
* Fix transcoding issues with some videos
|
||||
* Fix Mac OS mkv/avi upload
|
||||
* Fix menu overflow on mobile
|
||||
* Fix ownership button icons ([@joshmorel](https://github.com/joshmorel))
|
||||
|
||||
|
||||
## v1.1.0
|
||||
|
||||
***Since v1.0.1***
|
||||
|
||||
### BREAKING CHANGES
|
||||
|
||||
* **Docker:** `PEERTUBE_TRUST_PROXY` env variable is now an array ([LecygneNoir](https://github.com/LecygneNoir))
|
||||
|
||||
### Maintenance
|
||||
|
|
|
@ -133,7 +133,7 @@ You can also join the cheerful bunch that makes our community:
|
|||
|
||||
* Chat<a name="contact"></a>:
|
||||
* IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
|
||||
* Matrix (bridged on the IRC channel) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
|
||||
* Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
|
||||
* Forum:
|
||||
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "peertube-client",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"licence": "GPLv3",
|
||||
"author": {
|
||||
|
@ -28,7 +28,8 @@
|
|||
"resolutions": {
|
||||
"video.js": "^7",
|
||||
"webtorrent/create-torrent/junk": "^1",
|
||||
"simple-get": "^2.8.1"
|
||||
"simple-get": "^2.8.1",
|
||||
"punycode": "^1.4.1"
|
||||
},
|
||||
"jest": {
|
||||
"globals": {
|
||||
|
@ -63,20 +64,20 @@
|
|||
"setupTestFrameworkScriptFile": "<rootDir>/src/setupJest.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "~0.11.1",
|
||||
"@angular/animations": "~7.1.1",
|
||||
"@angular/cli": "~7.1.1",
|
||||
"@angular/common": "~7.1.1",
|
||||
"@angular/compiler": "~7.1.1",
|
||||
"@angular/compiler-cli": "~7.1.1",
|
||||
"@angular/core": "~7.1.1",
|
||||
"@angular/forms": "~7.1.1",
|
||||
"@angular/http": "~7.1.1",
|
||||
"@angular/language-service": "~7.1.1",
|
||||
"@angular/platform-browser": "~7.1.1",
|
||||
"@angular/platform-browser-dynamic": "~7.1.1",
|
||||
"@angular/router": "~7.1.1",
|
||||
"@angular/service-worker": "~7.1.1",
|
||||
"@angular-devkit/build-angular": "~0.13.1",
|
||||
"@angular/animations": "~7.2.4",
|
||||
"@angular/cli": "~7.3.1",
|
||||
"@angular/common": "~7.2.4",
|
||||
"@angular/compiler": "~7.2.4",
|
||||
"@angular/compiler-cli": "~7.2.4",
|
||||
"@angular/core": "~7.2.4",
|
||||
"@angular/forms": "~7.2.4",
|
||||
"@angular/http": "~7.2.4",
|
||||
"@angular/language-service": "~7.2.4",
|
||||
"@angular/platform-browser": "~7.2.4",
|
||||
"@angular/platform-browser-dynamic": "~7.2.4",
|
||||
"@angular/router": "~7.2.4",
|
||||
"@angular/service-worker": "~7.2.4",
|
||||
"@angularclass/hmr": "^2.1.3",
|
||||
"@neos21/bootstrap3-glyphicons": "^1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^4.0.0",
|
||||
|
@ -85,7 +86,9 @@
|
|||
"@ngx-loading-bar/router": "^3.0.0",
|
||||
"@ngx-meta/core": "^6.0.0-rc.1",
|
||||
"@ngx-translate/i18n-polyfill": "^1.0.0",
|
||||
"@streamroot/videojs-hlsjs-plugin": "^1.0.7",
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/hls.js": "^0.12.0",
|
||||
"@types/jasmine": "^2.8.7",
|
||||
"@types/jasminewd2": "^2.0.3",
|
||||
"@types/jest": "^23.3.1",
|
||||
|
@ -109,6 +112,7 @@
|
|||
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"focus-visible": "^4.1.5",
|
||||
"hls.js": "^0.12.2",
|
||||
"html-loader": "^0.5.5",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
|
@ -131,6 +135,7 @@
|
|||
"ngx-qrcode2": "^0.0.9",
|
||||
"node-sass": "^4.9.3",
|
||||
"npm-font-source-sans-pro": "^1.0.2",
|
||||
"p2p-media-loader-hlsjs": "^0.4.0",
|
||||
"path-browserify": "^1.0.0",
|
||||
"primeng": "^7.0.0",
|
||||
"process": "^0.11.10",
|
||||
|
@ -152,9 +157,9 @@
|
|||
"typescript": "3.1.6",
|
||||
"video.js": "^7",
|
||||
"videojs-contextmenu-ui": "^5.0.0",
|
||||
"videojs-contrib-quality-levels": "^2.0.9",
|
||||
"videojs-dock": "^2.0.2",
|
||||
"videojs-hotkeys": "^0.2.21",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-bundle-analyzer": "^3.0.2",
|
||||
"webpack-cli": "^3.0.8",
|
||||
"webtorrent": "https://github.com/webtorrent/webtorrent#e9b209c7970816fc29e0cc871157a4918d66001d",
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core'
|
||||
import { Notifier, ServerService } from '@app/core'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { ContactAdminModalComponent } from '@app/+about/about-instance/contact-admin-modal.component'
|
||||
import { InstanceService } from '@app/shared/instance/instance.service'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-about-instance',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<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 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 { AccountService } from '@app/shared/account/account.service'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-account-about',
|
||||
|
|
|
@ -26,8 +26,7 @@ export class AccountsComponent implements OnInit, OnDestroy {
|
|||
private notifier: Notifier,
|
||||
private restExtractor: RestExtractor,
|
||||
private redirectService: RedirectService,
|
||||
private authService: AuthService,
|
||||
private i18n: I18n
|
||||
private authService: AuthService
|
||||
) {}
|
||||
|
||||
ngOnInit () {
|
||||
|
|
|
@ -10,7 +10,7 @@ import { FollowingListComponent } from './follows/following-list/following-list.
|
|||
import { JobsComponent } from './jobs/job.component'
|
||||
import { JobsListComponent } from './jobs/jobs-list/jobs-list.component'
|
||||
import { JobService } from './jobs/shared/job.service'
|
||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent } from './users'
|
||||
import { UserCreateComponent, UserListComponent, UsersComponent, UserUpdateComponent, UserPasswordComponent } from './users'
|
||||
import { ModerationCommentModalComponent, VideoAbuseListComponent, VideoBlacklistListComponent } from './moderation'
|
||||
import { ModerationComponent } from '@app/+admin/moderation/moderation.component'
|
||||
import { RedundancyCheckboxComponent } from '@app/+admin/follows/shared/redundancy-checkbox.component'
|
||||
|
@ -36,6 +36,7 @@ import { InstanceAccountBlocklistComponent, InstanceServerBlocklistComponent } f
|
|||
UsersComponent,
|
||||
UserCreateComponent,
|
||||
UserUpdateComponent,
|
||||
UserPasswordComponent,
|
||||
UserListComponent,
|
||||
|
||||
ModerationComponent,
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
font-weight: $font-semibold;
|
||||
min-width: 200px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.moderation-expanded-text {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Moderation comment</h4>
|
||||
<span class="close" aria-hidden="true" (click)="hideModerationCommentModal()"></span>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
|
@ -14,12 +15,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div i18n>
|
||||
<div class="form-group" i18n>
|
||||
This comment can only be seen by you or the other moderators.
|
||||
</div>
|
||||
|
||||
<div class="form-group inputs">
|
||||
<span i18n class="action-button action-button-cancel" (click)="hideModerationCommentModal()">Cancel</span>
|
||||
<span i18n class="action-button action-button-cancel" (click)="hide()">Cancel</span>
|
||||
|
||||
<input
|
||||
type="submit" i18n-value value="Update this comment" class="action-button-submit"
|
||||
|
|
|
@ -45,7 +45,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
|||
})
|
||||
}
|
||||
|
||||
hideModerationCommentModal () {
|
||||
hide () {
|
||||
this.abuseToComment = undefined
|
||||
this.openedModal.close()
|
||||
this.form.reset()
|
||||
|
@ -60,7 +60,7 @@ export class ModerationCommentModalComponent extends FormReactive implements OnI
|
|||
this.notifier.success(this.i18n('Comment updated.'))
|
||||
|
||||
this.commentUpdated.emit(moderationComment)
|
||||
this.hideModerationCommentModal()
|
||||
this.hide()
|
||||
},
|
||||
|
||||
err => this.notifier.error(err.message)
|
||||
|
|
|
@ -51,11 +51,11 @@
|
|||
<td class="moderation-expanded" colspan="6">
|
||||
<div>
|
||||
<span i18n class="moderation-expanded-label">Reason:</span>
|
||||
<span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
|
||||
</div>
|
||||
<div *ngIf="videoAbuse.moderationComment">
|
||||
<span i18n class="moderation-expanded-label">Moderation comment:</span>
|
||||
<span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DropdownAction } from '../../../shared/buttons/action-dropdown.componen
|
|||
import { ConfirmService } from '../../../core/index'
|
||||
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
|
||||
import { Video } from '../../../shared/video/video.model'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-abuse-list',
|
||||
|
@ -30,7 +31,8 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
private notifier: Notifier,
|
||||
private videoAbuseService: VideoAbuseService,
|
||||
private confirmService: ConfirmService,
|
||||
private i18n: I18n
|
||||
private i18n: I18n,
|
||||
private markdownRenderer: MarkdownService
|
||||
) {
|
||||
super()
|
||||
|
||||
|
@ -108,6 +110,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
|
|||
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||
}
|
||||
|
||||
protected loadData () {
|
||||
return this.videoAbuseService.getVideoAbuses(this.pagination, this.sort)
|
||||
.subscribe(
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<th style="width: 40px"></th>
|
||||
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
|
||||
<th i18n>Sensitive</th>
|
||||
<th i18n>Unfederated</th>
|
||||
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
|
||||
<th style="width: 120px;"></th>
|
||||
</tr>
|
||||
|
@ -26,7 +27,8 @@
|
|||
</a>
|
||||
</td>
|
||||
|
||||
<td>{{ videoBlacklist.video.nsfw }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.video.nsfw) }}</td>
|
||||
<td>{{ booleanToText(videoBlacklist.unfederated) }}</td>
|
||||
<td>{{ videoBlacklist.createdAt }}</td>
|
||||
|
||||
<td class="action-cell">
|
||||
|
@ -37,9 +39,9 @@
|
|||
|
||||
<ng-template pTemplate="rowexpansion" let-videoBlacklist>
|
||||
<tr>
|
||||
<td class="moderation-expanded" colspan="5">
|
||||
<td class="moderation-expanded" colspan="6">
|
||||
<span i18n class="moderation-expanded-label">Blacklist reason:</span>
|
||||
<span class="moderation-expanded-text">{{ videoBlacklist.reason }}</span>
|
||||
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoBlacklist.reason)"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
|
|
@ -7,6 +7,7 @@ import { VideoBlacklist } from '../../../../../../shared'
|
|||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { DropdownAction } from '../../../shared/buttons/action-dropdown.component'
|
||||
import { Video } from '../../../shared/video/video.model'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-blacklist-list',
|
||||
|
@ -26,6 +27,7 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
private notifier: Notifier,
|
||||
private confirmService: ConfirmService,
|
||||
private videoBlacklistService: VideoBlacklistService,
|
||||
private markdownRenderer: MarkdownService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
|
@ -46,6 +48,16 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
|
|||
return Video.buildClientUrl(videoBlacklist.video.uuid)
|
||||
}
|
||||
|
||||
booleanToText (value: boolean) {
|
||||
if (value === true) return this.i18n('yes')
|
||||
|
||||
return this.i18n('no')
|
||||
}
|
||||
|
||||
toHtml (text: string) {
|
||||
return this.markdownRenderer.textMarkdownToHTML(text)
|
||||
}
|
||||
|
||||
async removeVideoFromBlacklist (entry: VideoBlacklist) {
|
||||
const confirmMessage = this.i18n(
|
||||
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './user-create.component'
|
||||
export * from './user-update.component'
|
||||
export * from './user-password.component'
|
||||
|
|
|
@ -81,3 +81,17 @@
|
|||
|
||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</form>
|
||||
|
||||
<div *ngIf="!isCreation()" class="danger-zone">
|
||||
<div class="account-title" i18n>Danger Zone</div>
|
||||
|
||||
<div class="form-group reset-password-email">
|
||||
<label i18n>Send a link to reset the password by email to the user</label>
|
||||
<button (click)="resetPassword()" i18n>Ask for new password</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label i18n>Manually set the user password</label>
|
||||
<my-user-password [userId]="userId"></my-user-password>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@ input:not([type=submit]) {
|
|||
@include peertube-select-container(340px);
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
input[type=submit], button {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
|
||||
|
@ -25,3 +25,23 @@ input[type=submit] {
|
|||
margin-top: 5px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
@include in-content-small-title;
|
||||
|
||||
margin-top: 55px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
.reset-password-email {
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
button {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export abstract class UserEdit extends FormReactive {
|
|||
videoQuotaDailyOptions: { value: string, label: string }[] = []
|
||||
roles = Object.keys(USER_ROLE_LABELS).map(key => ({ value: key.toString(), label: USER_ROLE_LABELS[key] }))
|
||||
username: string
|
||||
userId: number
|
||||
|
||||
protected abstract serverService: ServerService
|
||||
protected abstract configService: ConfigService
|
||||
|
@ -22,7 +23,9 @@ export abstract class UserEdit extends FormReactive {
|
|||
}
|
||||
|
||||
computeQuotaWithTranscoding () {
|
||||
const resolutions = this.serverService.getConfig().transcoding.enabledResolutions
|
||||
const transcodingConfig = this.serverService.getConfig().transcoding
|
||||
|
||||
const resolutions = transcodingConfig.enabledResolutions
|
||||
const higherResolution = VideoResolution.H_1080P
|
||||
let multiplier = 0
|
||||
|
||||
|
@ -30,9 +33,15 @@ export abstract class UserEdit extends FormReactive {
|
|||
multiplier += resolution / higherResolution
|
||||
}
|
||||
|
||||
if (transcodingConfig.hls.enabled) multiplier *= 2
|
||||
|
||||
return multiplier * parseInt(this.form.value['videoQuota'], 10)
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
return
|
||||
}
|
||||
|
||||
protected buildQuotaOptions () {
|
||||
// These are used by a HTML select, so convert key into strings
|
||||
this.videoQuotaOptions = this.configService
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
|
||||
<div class="form-group">
|
||||
|
||||
<div class="input-group">
|
||||
<input id="password" [attr.type]="showPassword ? 'text' : 'password'"
|
||||
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
|
||||
>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
|
||||
<ng-container *ngIf="!showPassword" i18n>Show</ng-container>
|
||||
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="formErrors.password" class="form-error">
|
||||
{{ formErrors.password }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
|
||||
</form>
|
|
@ -0,0 +1,22 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
input:not([type=submit]):not([type=checkbox]) {
|
||||
@include peertube-input-text(340px);
|
||||
|
||||
display: block;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
input[type=submit] {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.input-group-append {
|
||||
height: 30px;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { Component, Input, OnInit } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { UserService } from '@app/shared/users/user.service'
|
||||
import { Notifier } from '../../../core'
|
||||
import { User, UserUpdate } from '../../../../../../shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
|
||||
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
|
||||
import { FormReactive } from '../../../shared'
|
||||
|
||||
@Component({
|
||||
selector: 'my-user-password',
|
||||
templateUrl: './user-password.component.html',
|
||||
styleUrls: [ './user-password.component.scss' ]
|
||||
})
|
||||
export class UserPasswordComponent extends FormReactive implements OnInit {
|
||||
error: string
|
||||
username: string
|
||||
showPassword = false
|
||||
|
||||
@Input() userId: number
|
||||
|
||||
constructor (
|
||||
protected formValidatorService: FormValidatorService,
|
||||
private userValidatorsService: UserValidatorsService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private notifier: Notifier,
|
||||
private userService: UserService,
|
||||
private i18n: I18n
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
ngOnInit () {
|
||||
this.buildForm({
|
||||
password: this.userValidatorsService.USER_PASSWORD
|
||||
})
|
||||
}
|
||||
|
||||
formValidated () {
|
||||
this.error = undefined
|
||||
|
||||
const userUpdate: UserUpdate = this.form.value
|
||||
|
||||
this.userService.updateUser(this.userId, userUpdate).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('Password changed for user {{username}}.', { username: this.username })
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
togglePasswordVisibility () {
|
||||
this.showPassword = !this.showPassword
|
||||
}
|
||||
|
||||
getFormButtonTitle () {
|
||||
return this.i18n('Update user password')
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import { UserService } from '@app/shared'
|
|||
export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
||||
error: string
|
||||
userId: number
|
||||
userEmail: string
|
||||
username: string
|
||||
|
||||
private paramsSub: Subscription
|
||||
|
@ -89,9 +90,22 @@ export class UserUpdateComponent extends UserEdit implements OnInit, OnDestroy {
|
|||
return this.i18n('Update user')
|
||||
}
|
||||
|
||||
resetPassword () {
|
||||
this.userService.askResetPassword(this.userEmail).subscribe(
|
||||
() => {
|
||||
this.notifier.success(
|
||||
this.i18n('An email asking for password reset has been sent to {{username}}.', { username: this.username })
|
||||
)
|
||||
},
|
||||
|
||||
err => this.error = err.message
|
||||
)
|
||||
}
|
||||
|
||||
private onUserFetched (userJson: User) {
|
||||
this.userId = userJson.id
|
||||
this.username = userJson.username
|
||||
this.userEmail = userJson.email
|
||||
|
||||
this.form.patchValue({
|
||||
email: userJson.email,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div i18n class="form-sub-title">Users list</div>
|
||||
|
||||
<a class="add-button" routerLink="/admin/users/create">
|
||||
<span class="icon icon-add"></span>
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create user</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -65,7 +65,9 @@
|
|||
<span i18n *ngIf="user.blocked" class="banned-info">(banned)</span>
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td *ngIf="!requiresEmailVerification || user.blocked; else emailWithVerificationStatus">{{ user.email }}</td>
|
||||
|
||||
<ng-template #emailWithVerificationStatus>
|
||||
<td *ngIf="user.emailVerified === false; else emailVerifiedNotFalse" i18n-title title="User's email must be verified to login">
|
||||
<em>? {{ user.email }}</em>
|
||||
|
@ -76,6 +78,7 @@
|
|||
</td>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
|
||||
<td>{{ user.videoQuotaUsed }} / {{ user.videoQuota }}</td>
|
||||
<td>{{ user.roleLabel }}</td>
|
||||
<td>{{ user.createdAt }}</td>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import '_mixins';
|
||||
|
||||
.add-button {
|
||||
@include create-button('../../../../assets/images/global/add.svg');
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
tr.banned {
|
||||
|
|
|
@ -65,10 +65,10 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: #585858;
|
||||
color: $grey-foreground-color;
|
||||
|
||||
&:hover {
|
||||
color: #303030;
|
||||
color: $grey-foreground-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<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>
|
||||
|
||||
<my-user-notifications #userNotification></my-user-notifications>
|
||||
|
|
|
@ -5,16 +5,18 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
a {
|
||||
@include peertube-button-link;
|
||||
@include grey-button;
|
||||
@include button-with-icon(18px, 3px, -1px);
|
||||
}
|
||||
|
||||
button {
|
||||
@include peertube-button;
|
||||
@include grey-button;
|
||||
@include button-with-icon(20px, 3px, -1px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<ng-template #modal let-close="close" let-dismiss="dismiss">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Accept ownership</h4>
|
||||
<span class="close" aria-label="Close" role="button" (click)="dismiss()"></span>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" [formGroup]="form">
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
<td class="action-cell">
|
||||
<ng-container *ngIf="videoChangeOwnership.status === 'WAITING'">
|
||||
<my-button i18n label="Accept"
|
||||
icon="icon-tick"
|
||||
icon="tick"
|
||||
(click)="openAcceptModal(videoChangeOwnership)"></my-button>
|
||||
<my-button i18n label="Refuse"
|
||||
icon="icon-cross"
|
||||
icon="cross"
|
||||
(click)="refuse(videoChangeOwnership)">Refuse</my-button>
|
||||
</ng-container>
|
||||
</td>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="video-channels-header">
|
||||
<a class="create-button" routerLink="create">
|
||||
<span class="icon icon-add"></span>
|
||||
<my-global-icon iconName="add"></my-global-icon>
|
||||
<ng-container i18n>Create another video channel</ng-container>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
@import '_mixins';
|
||||
|
||||
.create-button {
|
||||
@include create-button('../../../assets/images/global/add.svg');
|
||||
@include create-button;
|
||||
}
|
||||
|
||||
/deep/ .action-button {
|
||||
|
|
|
@ -35,10 +35,14 @@ export class MyAccountVideoChannelsComponent implements OnInit {
|
|||
async deleteVideoChannel (videoChannel: VideoChannel) {
|
||||
const res = await this.confirmService.confirmWithInput(
|
||||
this.i18n(
|
||||
'Do you really want to delete {{videoChannelName}}? It will delete all videos uploaded in this channel too.',
|
||||
{ videoChannelName: videoChannel.displayName }
|
||||
'Do you really want to delete {{channelDisplayName}}? It will delete all videos uploaded in this channel, ' +
|
||||
'and you will not be able to create another channel with the same name ({{channelName}})!',
|
||||
{ channelDisplayName: videoChannel.displayName, channelName: videoChannel.name }
|
||||
),
|
||||
this.i18n(
|
||||
'Please type the display name of the video channel ({{displayName}}) to confirm',
|
||||
{ displayName: videoChannel.displayName }
|
||||
),
|
||||
this.i18n('Please type the name of the video channel to confirm'),
|
||||
videoChannel.displayName,
|
||||
this.i18n('Delete')
|
||||
)
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</span>
|
||||
|
||||
<span class="action-button action-button-delete-selection" (click)="deleteSelectedVideos()">
|
||||
<span class="icon icon-delete-white"></span>
|
||||
<my-global-icon iconName="delete"></my-global-icon>
|
||||
<ng-container i18n>Delete</ng-container>
|
||||
</span>
|
||||
</div>
|
||||
|
@ -45,7 +45,7 @@
|
|||
|
||||
<my-button i18n-label label="Change ownership"
|
||||
className="action-button-change-ownership"
|
||||
icon="icon-im-with-her"
|
||||
icon="im-with-her"
|
||||
(click)="changeOwnership($event, video)"
|
||||
></my-button>
|
||||
</div>
|
||||
|
|
|
@ -23,14 +23,11 @@
|
|||
.action-button-delete-selection {
|
||||
@include peertube-button;
|
||||
@include orange-button;
|
||||
@include button-with-icon(21px);
|
||||
|
||||
my-global-icon {
|
||||
@include apply-svg-color(#fff);
|
||||
}
|
||||
|
||||
.icon.icon-delete-white {
|
||||
@include icon(21px);
|
||||
|
||||
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">
|
||||
<div class="modal-header">
|
||||
<h4 i18n class="modal-title">Change ownership</h4>
|
||||
<span class="close" aria-label="Close" role="button" (click)="dismiss()"></span>
|
||||
|
||||
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" [formGroup]="form">
|
||||
|
|
|
@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
|
|||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-about',
|
||||
|
|
|
@ -30,14 +30,16 @@
|
|||
|
||||
<footer class="row">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
|
||||
|
||||
<my-confirm></my-confirm>
|
||||
|
||||
<p-toast position="bottom-right">
|
||||
<ng-template let-message pTemplate="message">
|
||||
<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 { Injectable } from '@angular/core'
|
||||
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 { User } from '../../../../../shared/models/users'
|
||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||
import { environment } from '../../../environments/environment'
|
||||
import { RestExtractor } from '../../shared/rest'
|
||||
import { RestExtractor } from '../../shared/rest/rest-extractor.service'
|
||||
import { AuthStatus } from './auth-status.model'
|
||||
import { AuthUser } from './auth-user.model'
|
||||
import { objectToUrlEncoded } from '@app/shared/misc/utils'
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
export * from './confirm.component'
|
||||
export * from './confirm.service'
|
||||
|
|
|
@ -8,7 +8,7 @@ import { LoadingBarHttpClientModule } from '@ngx-loading-bar/http-client'
|
|||
import { LoadingBarRouterModule } from '@ngx-loading-bar/router'
|
||||
|
||||
import { AuthService } from './auth'
|
||||
import { ConfirmComponent, ConfirmService } from './confirm'
|
||||
import { ConfirmService } from './confirm'
|
||||
import { throwIfAlreadyLoaded } from './module-import-guard'
|
||||
import { LoginGuard, RedirectService, UserRightGuard } from './routing'
|
||||
import { ServerService } from './server'
|
||||
|
@ -18,6 +18,7 @@ import { CheatSheetComponent } from './hotkeys'
|
|||
import { ToastModule } from 'primeng/toast'
|
||||
import { Notifier } from './notification'
|
||||
import { MessageService } from 'primeng/api'
|
||||
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@ -37,7 +38,6 @@ import { MessageService } from 'primeng/api'
|
|||
],
|
||||
|
||||
declarations: [
|
||||
ConfirmComponent,
|
||||
CheatSheetComponent
|
||||
],
|
||||
|
||||
|
@ -47,7 +47,6 @@ import { MessageService } from 'primeng/api'
|
|||
|
||||
ToastModule,
|
||||
|
||||
ConfirmComponent,
|
||||
CheatSheetComponent
|
||||
],
|
||||
|
||||
|
@ -60,7 +59,8 @@ import { MessageService } from 'primeng/api'
|
|||
UserRightGuard,
|
||||
RedirectService,
|
||||
Notifier,
|
||||
MessageService
|
||||
MessageService,
|
||||
UserNotificationSocket
|
||||
]
|
||||
})
|
||||
export class CoreModule {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
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
|
||||
},
|
||||
transcoding: {
|
||||
enabledResolutions: []
|
||||
enabledResolutions: [],
|
||||
hls: {
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
avatar: {
|
||||
file: {
|
||||
|
@ -87,6 +90,11 @@ export class ServerService {
|
|||
enabled: false
|
||||
}
|
||||
}
|
||||
},
|
||||
trending: {
|
||||
videos: {
|
||||
intervalDays: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
private videoCategories: Array<VideoConstant<number>> = []
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
<span (click)="doSearch()" class="icon icon-search"></span>
|
||||
|
||||
<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>
|
||||
</a>
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
padding-left: 10px;
|
||||
margin-right: 15px;
|
||||
padding-right: 40px; // For the search icon
|
||||
font-size: 14px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--inputPlaceholderColor);
|
||||
|
@ -40,6 +41,7 @@
|
|||
.upload-button {
|
||||
@include peertube-button-link;
|
||||
@include orange-button;
|
||||
@include button-with-icon(22px, 3px, -1px);
|
||||
|
||||
margin-right: 25px;
|
||||
|
||||
|
@ -47,15 +49,6 @@
|
|||
margin-right: 0;
|
||||
}
|
||||
|
||||
.icon.icon-upload {
|
||||
@include icon(22px);
|
||||
|
||||
background-image: url('../../assets/images/header/upload-white.svg');
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-right: 10px;
|
||||
padding: 0 10px;
|
||||
|
|
|
@ -55,7 +55,8 @@
|
|||
<ng-template #forgotPasswordModal>
|
||||
<div class="modal-header">
|
||||
<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 class="modal-body">
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
></a>
|
||||
</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>
|
||||
</ng-template>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
/deep/ {
|
||||
.popover-notifications.popover {
|
||||
max-width: 400px;
|
||||
max-width: none;
|
||||
|
||||
.popover-body {
|
||||
padding: 0;
|
||||
|
@ -11,6 +11,7 @@
|
|||
font-family: $main-fonts;
|
||||
overflow-y: auto;
|
||||
max-height: 500px;
|
||||
width: 400px;
|
||||
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.30);
|
||||
|
||||
.notifications-header {
|
||||
|
@ -40,7 +41,7 @@
|
|||
justify-content: center;
|
||||
font-weight: $font-semibold;
|
||||
color: var(--mainForegroundColor);
|
||||
height: 30px;
|
||||
padding: 7px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +72,7 @@
|
|||
justify-content: center;
|
||||
|
||||
background-color: var(--mainColor);
|
||||
color: var(--mainBackgroundColor);
|
||||
color: var(#fff);
|
||||
font-size: 10px;
|
||||
font-weight: $font-semibold;
|
||||
|
||||
|
@ -80,3 +81,11 @@
|
|||
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 { UserNotificationService } from '@app/shared/users/user-notification.service'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { Notifier } from '@app/core'
|
||||
import { Notifier, UserNotificationSocket } from '@app/core'
|
||||
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NavigationEnd, Router } from '@angular/router'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
@ -23,6 +23,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
|
|||
|
||||
constructor (
|
||||
private userNotificationService: UserNotificationService,
|
||||
private userNotificationSocket: UserNotificationSocket,
|
||||
private notifier: Notifier,
|
||||
private router: Router
|
||||
) {}
|
||||
|
@ -53,7 +54,7 @@ export class AvatarNotificationComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private subscribeToNotifications () {
|
||||
this.notificationSub = this.userNotificationService.getMyNotificationsSocket()
|
||||
this.notificationSub = this.userNotificationSocket.getMyNotificationsSocket()
|
||||
.subscribe(data => {
|
||||
if (data.type === 'new') return this.unreadNotifications++
|
||||
if (data.type === 'read') return this.unreadNotifications--
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<ng-template #modal let-hide="close">
|
||||
<div class="modal-header">
|
||||
<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>
|
||||
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ menu {
|
|||
height: 100%;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
color: var(--menuForegroundColor);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
@ -243,7 +243,7 @@ menu {
|
|||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 400px) {
|
||||
@media screen and (max-width: $mobile-view) {
|
||||
.menu-wrapper {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<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;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
color: #585858;
|
||||
color: $grey-foreground-color;
|
||||
|
||||
&:hover {
|
||||
color: #303030;
|
||||
color: $grey-foreground-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||
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 { SearchService } from '@app/search/search.service'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
|
@ -41,7 +41,8 @@ export class SearchComponent implements OnInit, OnDestroy {
|
|||
private metaService: MetaService,
|
||||
private notifier: Notifier,
|
||||
private searchService: SearchService,
|
||||
private authService: AuthService
|
||||
private authService: AuthService,
|
||||
private serverService: ServerService
|
||||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
|
@ -75,6 +76,10 @@ export class SearchComponent implements OnInit, OnDestroy {
|
|||
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 {
|
||||
return d instanceof VideoChannel
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ export abstract class Actor implements ActorServer {
|
|||
|
||||
avatarUrl: string
|
||||
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatar: Avatar }) {
|
||||
static GET_ACTOR_AVATAR_URL (actor: { avatar?: { path: string } }) {
|
||||
const absoluteAPIUrl = getAbsoluteAPIUrl()
|
||||
|
||||
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' }"
|
||||
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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -24,14 +24,11 @@
|
|||
}
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: $grey-color;
|
||||
background-color: $grey-background-color;
|
||||
}
|
||||
|
||||
.icon-action {
|
||||
@include icon(21px);
|
||||
|
||||
background-image: url('../../../assets/images/video/more.svg');
|
||||
top: -1px;
|
||||
.more-icon {
|
||||
width: 21px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<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>
|
||||
|
|
|
@ -3,41 +3,18 @@
|
|||
|
||||
.action-button {
|
||||
@include peertube-button-link;
|
||||
@include button-with-icon(21px, 0, -2px);
|
||||
|
||||
font-size: 15px;
|
||||
font-weight: $font-semibold;
|
||||
color: #585858;
|
||||
background-color: #E5E5E5;
|
||||
color: $grey-foreground-color;
|
||||
background-color: $grey-background-color;
|
||||
|
||||
&:hover {
|
||||
background-color: #EFEFEF;
|
||||
background-color: $grey-background-hover-color;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon(21px);
|
||||
|
||||
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');
|
||||
}
|
||||
my-global-icon {
|
||||
@include apply-svg-color($grey-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, Input } from '@angular/core'
|
||||
import { GlobalIconName } from '@app/shared/icons/global-icon.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-button',
|
||||
|
@ -9,7 +10,7 @@ import { Component, Input } from '@angular/core'
|
|||
export class ButtonComponent {
|
||||
@Input() label = ''
|
||||
@Input() className: string = undefined
|
||||
@Input() icon: string = undefined
|
||||
@Input() icon: GlobalIconName = undefined
|
||||
@Input() title: string = undefined
|
||||
|
||||
getTitle () {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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" i18n *ngIf="!label">Delete</span>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 i18n class="button-label" *ngIf="!label">Edit</span>
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
<div class="modal-header">
|
||||
<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 class="modal-body" >
|
|
@ -1,5 +1,5 @@
|
|||
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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap/modal/modal-ref'
|
|
@ -10,20 +10,20 @@ export class VideoAbuseValidatorsService {
|
|||
|
||||
constructor (private i18n: I18n) {
|
||||
this.VIDEO_ABUSE_REASON = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||
MESSAGES: {
|
||||
'required': this.i18n('Report reason is required.'),
|
||||
'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 = {
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(300) ],
|
||||
VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ],
|
||||
MESSAGES: {
|
||||
'required': this.i18n('Moderation comment is required.'),
|
||||
'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 { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { Subject } from 'rxjs'
|
||||
import truncate from 'lodash-es/truncate'
|
||||
import { ScreenService } from '@app/shared/misc/screen.service'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-markdown-textarea',
|
||||
|
|
|
@ -53,6 +53,17 @@ export class ReactiveFileComponent implements OnInit, ControlValueAccessor {
|
|||
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.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"
|
||||
(onHidden)="onPopoverHidden()"
|
||||
(onShown)="onPopoverShown()"
|
||||
></span>
|
||||
>
|
||||
<my-global-icon iconName="help"></my-global-icon>
|
||||
</span>
|
||||
|
|
|
@ -2,13 +2,17 @@
|
|||
@import '_mixins';
|
||||
|
||||
.help-tooltip-button {
|
||||
@include icon(17px);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
|
||||
my-global-icon {
|
||||
width: 17px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
background-image: url('../../../assets/images/global/help.svg');
|
||||
border: none;
|
||||
margin: 5px;
|
||||
|
||||
@include apply-svg-color(var(--mainForegroundColor))
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ {
|
||||
|
@ -16,16 +20,21 @@
|
|||
max-width: 300px;
|
||||
|
||||
.popover-body {
|
||||
font-family: $main-fonts;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
font-size: 13px;
|
||||
font-family: $main-fonts;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
background-color: var(--mainBackgroundColor);
|
||||
color: var(--mainForegroundColor);
|
||||
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component, Input, OnChanges, OnInit } from '@angular/core'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||
import { MarkdownService } from '@app/shared/renderer'
|
||||
|
||||
@Component({
|
||||
selector: 'my-help',
|
||||
|
|
|
@ -102,12 +102,18 @@ function objectToFormData (obj: any, form?: FormData, namespace?: string) {
|
|||
return fd
|
||||
}
|
||||
|
||||
function lineFeedToHtml (obj: any, keyToNormalize: string) {
|
||||
function objectLineFeedToHtml (obj: any, keyToNormalize: string) {
|
||||
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) {
|
||||
const index = arr.indexOf(elem)
|
||||
if (index !== -1) arr.splice(index, 1)
|
||||
|
@ -131,6 +137,7 @@ function scrollToTop () {
|
|||
export {
|
||||
sortBy,
|
||||
durationToString,
|
||||
lineFeedToHtml,
|
||||
objectToUrlEncoded,
|
||||
getParameterByName,
|
||||
populateAsyncUserVideoChannels,
|
||||
|
@ -138,7 +145,7 @@ export {
|
|||
dateToHuman,
|
||||
immutableAssign,
|
||||
objectToFormData,
|
||||
lineFeedToHtml,
|
||||
objectLineFeedToHtml,
|
||||
removeElementFromArray,
|
||||
scrollToTop
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<ng-template #modal>
|
||||
<div class="modal-header">
|
||||
<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 class="modal-body">
|
||||
|
@ -19,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<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
|
||||
type="submit" i18n-value value="Ban this user" class="action-button-submit"
|
||||
|
|
|
@ -42,7 +42,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
|
|||
this.openedModal = this.modalService.open(this.modal)
|
||||
}
|
||||
|
||||
hideBanUserModal () {
|
||||
hide () {
|
||||
this.usersToBan = undefined
|
||||
this.openedModal.close()
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ export class UserBanModalComponent extends FormReactive implements OnInit {
|
|||
this.notifier.success(message)
|
||||
|
||||
this.userBanned.emit(this.usersToBan)
|
||||
this.hideBanUserModal()
|
||||
this.hide()
|
||||
},
|
||||
|
||||
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 { HelpComponent } from '@app/shared/misc/help.component'
|
||||
import { InfiniteScrollerDirective } from '@app/shared/video/infinite-scroller.directive'
|
||||
import { MarkdownService } from '@app/videos/shared'
|
||||
|
||||
import { BytesPipe, KeysPipe, NgPipesModule } from 'ngx-pipes'
|
||||
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 {
|
||||
CustomConfigValidatorsService,
|
||||
InstanceValidatorsService,
|
||||
LoginValidatorsService,
|
||||
ReactiveFileComponent,
|
||||
ResetPasswordValidatorsService,
|
||||
InstanceValidatorsService,
|
||||
TextareaAutoResizeDirective,
|
||||
UserValidatorsService,
|
||||
VideoAbuseValidatorsService,
|
||||
|
@ -67,6 +66,9 @@ import { UserHistoryService } from '@app/shared/users/user-history.service'
|
|||
import { UserNotificationService } from '@app/shared/users/user-notification.service'
|
||||
import { UserNotificationsComponent } from '@app/shared/users/user-notifications.component'
|
||||
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({
|
||||
imports: [
|
||||
|
@ -110,7 +112,9 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
|||
UserBanModalComponent,
|
||||
UserModerationDropdownComponent,
|
||||
TopMenuDropdownComponent,
|
||||
UserNotificationsComponent
|
||||
UserNotificationsComponent,
|
||||
ConfirmComponent,
|
||||
GlobalIconComponent
|
||||
],
|
||||
|
||||
exports: [
|
||||
|
@ -151,6 +155,8 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
|||
UserModerationDropdownComponent,
|
||||
TopMenuDropdownComponent,
|
||||
UserNotificationsComponent,
|
||||
ConfirmComponent,
|
||||
GlobalIconComponent,
|
||||
|
||||
NumberFormatterPipe,
|
||||
ObjectLengthPipe,
|
||||
|
@ -167,7 +173,6 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
|||
UserService,
|
||||
VideoService,
|
||||
AccountService,
|
||||
MarkdownService,
|
||||
VideoChannelService,
|
||||
VideoCaptionService,
|
||||
VideoImportService,
|
||||
|
@ -192,6 +197,10 @@ import { InstanceService } from '@app/shared/instance/instance.service'
|
|||
UserHistoryService,
|
||||
InstanceService,
|
||||
|
||||
MarkdownService,
|
||||
LinkifierService,
|
||||
HtmlRendererService,
|
||||
|
||||
I18nPrimengCalendarService,
|
||||
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 {
|
||||
id: number
|
||||
|
@ -6,10 +7,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
read: boolean
|
||||
|
||||
video?: VideoInfo & {
|
||||
channel: {
|
||||
id: number
|
||||
displayName: string
|
||||
}
|
||||
channel: ActorInfo & { avatarUrl?: string }
|
||||
}
|
||||
|
||||
videoImport?: {
|
||||
|
@ -23,10 +21,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
comment?: {
|
||||
id: number
|
||||
threadId: number
|
||||
account: {
|
||||
id: number
|
||||
displayName: string
|
||||
}
|
||||
account: ActorInfo & { avatarUrl?: string }
|
||||
video: VideoInfo
|
||||
}
|
||||
|
||||
|
@ -40,18 +35,11 @@ export class UserNotification implements UserNotificationServer {
|
|||
video: VideoInfo
|
||||
}
|
||||
|
||||
account?: {
|
||||
id: number
|
||||
displayName: string
|
||||
name: string
|
||||
}
|
||||
account?: ActorInfo & { avatarUrl?: string }
|
||||
|
||||
actorFollow?: {
|
||||
id: number
|
||||
follower: {
|
||||
name: string
|
||||
displayName: string
|
||||
}
|
||||
follower: ActorInfo & { avatarUrl?: string }
|
||||
following: {
|
||||
type: 'account' | 'channel'
|
||||
name: string
|
||||
|
@ -76,12 +64,22 @@ export class UserNotification implements UserNotificationServer {
|
|||
this.read = hash.read
|
||||
|
||||
this.video = hash.video
|
||||
if (this.video) this.setAvatarUrl(this.video.channel)
|
||||
|
||||
this.videoImport = hash.videoImport
|
||||
|
||||
this.comment = hash.comment
|
||||
if (this.comment) this.setAvatarUrl(this.comment.account)
|
||||
|
||||
this.videoAbuse = hash.videoAbuse
|
||||
|
||||
this.videoBlacklist = hash.videoBlacklist
|
||||
|
||||
this.account = hash.account
|
||||
if (this.account) this.setAvatarUrl(this.account)
|
||||
|
||||
this.actorFollow = hash.actorFollow
|
||||
if (this.actorFollow) this.setAvatarUrl(this.actorFollow.follower)
|
||||
|
||||
this.createdAt = hash.createdAt
|
||||
this.updatedAt = hash.updatedAt
|
||||
|
@ -97,6 +95,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
|
||||
case UserNotificationType.NEW_COMMENT_ON_MY_VIDEO:
|
||||
case UserNotificationType.COMMENT_MENTION:
|
||||
this.accountUrl = this.buildAccountUrl(this.comment.account)
|
||||
this.commentUrl = [ this.buildVideoUrl(this.comment.video), { threadId: this.comment.threadId } ]
|
||||
break
|
||||
|
||||
|
@ -138,8 +137,8 @@ export class UserNotification implements UserNotificationServer {
|
|||
return '/videos/watch/' + video.uuid
|
||||
}
|
||||
|
||||
private buildAccountUrl (account: { name: string }) {
|
||||
return '/accounts/' + account.name
|
||||
private buildAccountUrl (account: { name: string, host: string }) {
|
||||
return '/accounts/' + Actor.CREATE_BY_STRING(account.name, account.host)
|
||||
}
|
||||
|
||||
private buildVideoImportUrl () {
|
||||
|
@ -150,4 +149,7 @@ export class UserNotification implements UserNotificationServer {
|
|||
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 { 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 { environment } from '../../../environments/environment'
|
||||
import { ResultList, UserNotification as UserNotificationServer, UserNotificationSetting } from '../../../../../shared'
|
||||
import { UserNotification } from '@app/shared/users/user-notification.model'
|
||||
import { Subject } from 'rxjs'
|
||||
import * as io from 'socket.io-client'
|
||||
import { AuthService } from '@app/core'
|
||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||
import { User } from '@app/shared'
|
||||
import { UserNotification } from './user-notification.model'
|
||||
import { AuthService } from '../../core'
|
||||
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||
import { User } from '..'
|
||||
import { UserNotificationSocket } from '@app/core/notification/user-notification-socket.service'
|
||||
|
||||
@Injectable()
|
||||
export class UserNotificationService {
|
||||
static BASE_NOTIFICATIONS_URL = environment.apiUrl + '/api/v1/users/me/notifications'
|
||||
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 (
|
||||
private auth: AuthService,
|
||||
private authHttp: HttpClient,
|
||||
private restExtractor: RestExtractor,
|
||||
private restService: RestService
|
||||
private restService: RestService,
|
||||
private userNotificationSocket: UserNotificationSocket
|
||||
) {}
|
||||
|
||||
listMyNotifications (pagination: ComponentPagination, unread?: boolean, ignoreLoadingBar = false) {
|
||||
|
@ -48,16 +44,6 @@ export class UserNotificationService {
|
|||
.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) {
|
||||
const url = UserNotificationService.BASE_NOTIFICATIONS_URL + '/read'
|
||||
|
||||
|
@ -67,7 +53,7 @@ export class UserNotificationService {
|
|||
return this.authHttp.post(url, body, { headers })
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
tap(() => this.notificationSubject.next({ type: 'read' })),
|
||||
tap(() => this.userNotificationSocket.dispatch('read')),
|
||||
catchError(res => this.restExtractor.handleError(res))
|
||||
)
|
||||
}
|
||||
|
@ -79,7 +65,7 @@ export class UserNotificationService {
|
|||
return this.authHttp.post(url, {}, { headers })
|
||||
.pipe(
|
||||
map(this.restExtractor.extractDataBool),
|
||||
tap(() => this.notificationSubject.next({ type: 'read-all' })),
|
||||
tap(() => this.userNotificationSocket.dispatch('read-all')),
|
||||
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) {
|
||||
return new UserNotification(notification)
|
||||
}
|
||||
|
|
|
@ -1,61 +1,101 @@
|
|||
<div *ngIf="componentPagination.totalItems === 0" class="no-notification" i18n>You don't have notifications.</div>
|
||||
|
||||
<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">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.UNBLACKLIST_ON_MY_VIDEO">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.BLACKLIST_ON_MY_VIDEO">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.NEW_VIDEO_ABUSE_FOR_MODERATORS">
|
||||
<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 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 i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_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 i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_SUCCESS">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.MY_VIDEO_IMPORT_ERROR">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.NEW_USER_REGISTRATION">
|
||||
<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 i18n *ngSwitchCase="UserNotificationType.NEW_FOLLOW">
|
||||
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.actorFollow.follower.avatarUrl" />
|
||||
|
||||
<div class="message">
|
||||
<a (click)="markAsRead(notification)" [routerLink]="notification.accountUrl">{{ notification.actorFollow.follower.displayName }}</a> is following
|
||||
|
||||
<ng-container *ngIf="notification.actorFollow.following.type === 'channel'">
|
||||
your channel {{ notification.actorFollow.following.displayName }}
|
||||
</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 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>
|
||||
</ng-container>
|
||||
</div>
|
||||
<img alt="" aria-labelledby="avatar" class="avatar" [src]="notification.comment.account.avatarUrl" />
|
||||
|
||||
<div i18n title="Mark as read" class="mark-as-read">
|
||||
<div class="glyphicon glyphicon-ok" (click)="markAsRead(notification)"></div>
|
||||
<div class="message">
|
||||
<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>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<div class="from-date">{{ notification.createdAt | myFromNow }}</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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: inherit;
|
||||
padding: 15px 10px;
|
||||
padding: 15px 5px 15px 10px;
|
||||
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 {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:hover .mark-as-read .glyphicon {
|
||||
display: block;
|
||||
my-global-icon {
|
||||
width: 24px;
|
||||
margin-right: 11px;
|
||||
margin-left: 3px;
|
||||
|
||||
&:hover {
|
||||
color: rgba(20, 20, 20, 0.8);
|
||||
@include apply-svg-color(#333);
|
||||
}
|
||||
|
||||
.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 {
|
||||
@Input() ignoreLoadingBar = false
|
||||
@Input() infiniteScroll = true
|
||||
@Input() itemsPerPage = 20
|
||||
|
||||
notifications: UserNotification[] = []
|
||||
|
||||
// So we can access it in the template
|
||||
UserNotificationType = UserNotificationType
|
||||
|
||||
componentPagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 10,
|
||||
totalItems: null
|
||||
}
|
||||
componentPagination: ComponentPagination
|
||||
|
||||
constructor (
|
||||
private userNotificationService: UserNotificationService,
|
||||
|
@ -31,6 +28,12 @@ export class UserNotificationsComponent implements OnInit {
|
|||
) { }
|
||||
|
||||
ngOnInit () {
|
||||
this.componentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: this.itemsPerPage, // Reset items per page, because of the @Input() variable
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
this.loadMoreNotifications()
|
||||
}
|
||||
|
||||
|
@ -57,6 +60,8 @@ export class UserNotificationsComponent implements OnInit {
|
|||
}
|
||||
|
||||
markAsRead (notification: UserNotification) {
|
||||
if (notification.read) return
|
||||
|
||||
this.userNotificationService.markAsRead(notification)
|
||||
.subscribe(
|
||||
() => {
|
||||
|
|
|
@ -32,9 +32,7 @@ export class VideoAbuseService {
|
|||
|
||||
reportVideo (id: number, reason: string) {
|
||||
const url = VideoAbuseService.BASE_VIDEO_ABUSE_URL + id + '/abuse'
|
||||
const body = {
|
||||
reason
|
||||
}
|
||||
const body = { reason }
|
||||
|
||||
return this.authHttp.post(url, body)
|
||||
.pipe(
|
||||
|
|
|
@ -36,8 +36,11 @@ export class VideoBlacklistService {
|
|||
)
|
||||
}
|
||||
|
||||
blacklistVideo (videoId: number, reason?: string) {
|
||||
const body = reason ? { reason } : {}
|
||||
blacklistVideo (videoId: number, reason: string, unfederate: boolean) {
|
||||
const body = {
|
||||
unfederate,
|
||||
reason
|
||||
}
|
||||
|
||||
return this.authHttp.post(VideoBlacklistService.BASE_VIDEOS_URL + videoId + '/blacklist', body)
|
||||
.pipe(
|
||||
|
|
|
@ -82,6 +82,7 @@ export class VideoImportService {
|
|||
nsfw: video.nsfw,
|
||||
waitTranscoding: video.waitTranscoding,
|
||||
commentsEnabled: video.commentsEnabled,
|
||||
downloadEnabled: video.downloadEnabled,
|
||||
thumbnailfile: video.thumbnailfile,
|
||||
previewfile: video.previewfile,
|
||||
scheduleUpdate,
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<div [ngClass]="{ 'margin-content': marginContent }">
|
||||
<div class="videos-header">
|
||||
<div *ngIf="titlePage" class="title-page title-page-single">
|
||||
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
|
||||
{{ titlePage }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<my-feed [syndicationItems]="syndicationItems"></my-feed>
|
||||
|
||||
<div class="moderation-block" *ngIf="displayModerationBlock">
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
|
||||
my-feed {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.moderation-block {
|
||||
|
|
|
@ -39,6 +39,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
|
|||
ownerDisplayType: OwnerDisplayType = 'account'
|
||||
firstLoadedPage: number
|
||||
displayModerationBlock = false
|
||||
titleTooltip: string
|
||||
|
||||
protected baseVideoWidth = 215
|
||||
protected baseVideoHeight = 205
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<div class="video-feed">
|
||||
<span
|
||||
<my-global-icon
|
||||
*ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom"
|
||||
class="icon icon-syndication" role="button"
|
||||
></span>
|
||||
class="icon-syndication" role="button" iconName="syndication"
|
||||
>
|
||||
</my-global-icon>
|
||||
|
||||
<ng-template #feedsList>
|
||||
<a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import '_variables';
|
||||
@import '_mixins';
|
||||
|
||||
.video-feed {
|
||||
|
@ -6,14 +7,12 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include icon(12px);
|
||||
|
||||
&.icon-syndication {
|
||||
my-global-icon {
|
||||
cursor: pointer;
|
||||
width: 12px;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
background-color: var(--mainForegroundColor);
|
||||
mask-image: url('../../../assets/images/global/syndication.svg');
|
||||
}
|
||||
|
||||
@include apply-svg-color(var(--mainForegroundColor))
|
||||
}
|
||||
}
|
|
@ -3,6 +3,8 @@ import { AuthUser } from '../../core'
|
|||
import { Video } from '../../shared/video/video.model'
|
||||
import { Account } from '@app/shared/account/account.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 {
|
||||
descriptionPath: string
|
||||
|
@ -12,6 +14,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
files: VideoFile[]
|
||||
account: Account
|
||||
commentsEnabled: boolean
|
||||
downloadEnabled: boolean
|
||||
|
||||
waitTranscoding: boolean
|
||||
state: VideoConstant<VideoState>
|
||||
|
@ -19,6 +22,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
likesPercent: number
|
||||
dislikesPercent: number
|
||||
|
||||
trackerUrls: string[]
|
||||
|
||||
streamingPlaylists: VideoStreamingPlaylist[]
|
||||
|
||||
constructor (hash: VideoDetailsServerModel, translations = {}) {
|
||||
super(hash, translations)
|
||||
|
||||
|
@ -29,6 +36,10 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
this.tags = hash.tags
|
||||
this.support = hash.support
|
||||
this.commentsEnabled = hash.commentsEnabled
|
||||
this.downloadEnabled = hash.downloadEnabled
|
||||
|
||||
this.trackerUrls = hash.trackerUrls
|
||||
this.streamingPlaylists = hash.streamingPlaylists
|
||||
|
||||
this.buildLikeAndDislikePercents()
|
||||
}
|
||||
|
@ -53,4 +64,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
|||
this.likesPercent = (this.likes / (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[]
|
||||
nsfw: boolean
|
||||
commentsEnabled: boolean
|
||||
downloadEnabled: boolean
|
||||
waitTranscoding: boolean
|
||||
channelId: number
|
||||
privacy: VideoPrivacy
|
||||
|
@ -27,7 +28,15 @@ export class VideoEdit implements VideoUpdate {
|
|||
scheduleUpdate?: VideoScheduleUpdate
|
||||
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) {
|
||||
this.id = video.id
|
||||
this.uuid = video.uuid
|
||||
|
@ -39,6 +48,7 @@ export class VideoEdit implements VideoUpdate {
|
|||
this.tags = video.tags
|
||||
this.nsfw = video.nsfw
|
||||
this.commentsEnabled = video.commentsEnabled
|
||||
this.downloadEnabled = video.downloadEnabled
|
||||
this.waitTranscoding = video.waitTranscoding
|
||||
this.channelId = video.channel.id
|
||||
this.privacy = video.privacy.id
|
||||
|
@ -88,6 +98,7 @@ export class VideoEdit implements VideoUpdate {
|
|||
tags: this.tags,
|
||||
nsfw: this.nsfw,
|
||||
commentsEnabled: this.commentsEnabled,
|
||||
downloadEnabled: this.downloadEnabled,
|
||||
waitTranscoding: this.waitTranscoding,
|
||||
channelId: this.channelId,
|
||||
privacy: this.privacy,
|
||||
|
|
|
@ -50,10 +50,10 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
color: #585858;
|
||||
color: $grey-foreground-color;
|
||||
|
||||
&:hover {
|
||||
color: #303030;
|
||||
color: $grey-foreground-hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ export class Video implements VideoServerModel {
|
|||
displayName: string
|
||||
url: string
|
||||
host: string
|
||||
avatar: Avatar
|
||||
avatar?: Avatar
|
||||
}
|
||||
|
||||
channel: {
|
||||
|
@ -64,7 +64,7 @@ export class Video implements VideoServerModel {
|
|||
displayName: string
|
||||
url: string
|
||||
host: string
|
||||
avatar: Avatar
|
||||
avatar?: Avatar
|
||||
}
|
||||
|
||||
userHistory?: {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue