Merge branch 'develop' into pr/1285

This commit is contained in:
Chocobozzz 2019-02-11 14:09:23 +01:00
commit b718fd2237
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
387 changed files with 18290 additions and 10001 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,11 +51,11 @@
<td class="moderation-expanded" colspan="6">
<div>
<span i18n class="moderation-expanded-label">Reason:</span>
<span class="moderation-expanded-text">{{ videoAbuse.reason }}</span>
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.reason)"></span>
</div>
<div *ngIf="videoAbuse.moderationComment">
<span i18n class="moderation-expanded-label">Moderation comment:</span>
<span class="moderation-expanded-text">{{ videoAbuse.moderationComment }}</span>
<span class="moderation-expanded-text" [innerHTML]="toHtml(videoAbuse.moderationComment)"></span>
</div>
</td>
</tr>

View File

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

View File

@ -7,6 +7,7 @@
<th style="width: 40px"></th>
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
<th i18n>Sensitive</th>
<th i18n>Unfederated</th>
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 120px;"></th>
</tr>
@ -26,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>

View File

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

View File

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

View File

@ -81,3 +81,17 @@
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>
<div *ngIf="!isCreation()" class="danger-zone">
<div class="account-title" i18n>Danger Zone</div>
<div class="form-group reset-password-email">
<label i18n>Send a link to reset the password by email to the user</label>
<button (click)="resetPassword()" i18n>Ask for new password</button>
</div>
<div class="form-group">
<label i18n>Manually set the user password</label>
<my-user-password [userId]="userId"></my-user-password>
</div>
</div>

View File

@ -14,7 +14,7 @@ input:not([type=submit]) {
@include peertube-select-container(340px);
}
input[type=submit] {
input[type=submit], button {
@include peertube-button;
@include orange-button;
@ -25,3 +25,23 @@ input[type=submit] {
margin-top: 5px;
font-size: 11px;
}
.account-title {
@include in-content-small-title;
margin-top: 55px;
margin-bottom: 30px;
}
.danger-zone {
.reset-password-email {
margin-bottom: 30px;
padding-bottom: 30px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
button {
display: block;
margin-top: 0;
}
}
}

View File

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

View File

@ -0,0 +1,21 @@
<form role="form" (ngSubmit)="formValidated()" [formGroup]="form">
<div class="form-group">
<div class="input-group">
<input id="password" [attr.type]="showPassword ? 'text' : 'password'"
formControlName="password" [ngClass]="{ 'input-error': formErrors['password'] }"
>
<div class="input-group-append">
<button class="btn btn-sm btn-outline-secondary" (click)="togglePasswordVisibility()" type="button">
<ng-container *ngIf="!showPassword" i18n>Show</ng-container>
<ng-container *ngIf="!!showPassword" i18n>Hide</ng-container>
</button>
</div>
</div>
<div *ngIf="formErrors.password" class="form-error">
{{ formErrors.password }}
</div>
</div>
<input type="submit" value="{{ getFormButtonTitle() }}" [disabled]="!form.valid">
</form>

View File

@ -0,0 +1,22 @@
@import '_variables';
@import '_mixins';
input:not([type=submit]):not([type=checkbox]) {
@include peertube-input-text(340px);
display: block;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
input[type=submit] {
@include peertube-button;
@include orange-button;
margin-top: 10px;
}
.input-group-append {
height: 30px;
}

View File

@ -0,0 +1,64 @@
import { Component, Input, OnInit } from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { UserService } from '@app/shared/users/user.service'
import { Notifier } from '../../../core'
import { User, UserUpdate } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
import { UserValidatorsService } from '@app/shared/forms/form-validators/user-validators.service'
import { FormReactive } from '../../../shared'
@Component({
selector: 'my-user-password',
templateUrl: './user-password.component.html',
styleUrls: [ './user-password.component.scss' ]
})
export class UserPasswordComponent extends FormReactive implements OnInit {
error: string
username: string
showPassword = false
@Input() userId: number
constructor (
protected formValidatorService: FormValidatorService,
private userValidatorsService: UserValidatorsService,
private route: ActivatedRoute,
private router: Router,
private notifier: Notifier,
private userService: UserService,
private i18n: I18n
) {
super()
}
ngOnInit () {
this.buildForm({
password: this.userValidatorsService.USER_PASSWORD
})
}
formValidated () {
this.error = undefined
const userUpdate: UserUpdate = this.form.value
this.userService.updateUser(this.userId, userUpdate).subscribe(
() => {
this.notifier.success(
this.i18n('Password changed for user {{username}}.', { username: this.username })
)
},
err => this.error = err.message
)
}
togglePasswordVisibility () {
this.showPassword = !this.showPassword
}
getFormButtonTitle () {
return this.i18n('Update user password')
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,14 +23,11 @@
.action-button-delete-selection {
@include peertube-button;
@include orange-button;
}
@include button-with-icon(21px);
.icon.icon-delete-white {
@include icon(21px);
position: relative;
top: -2px;
background-image: url('../../../assets/images/global/delete-white.svg');
my-global-icon {
@include apply-svg-color(#fff);
}
}
}
}

View File

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

View File

@ -3,7 +3,7 @@ import { VideoChannelService } from '@app/shared/video-channel/video-channel.ser
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { Subscription } from 'rxjs'
import { MarkdownService } from '@app/videos/shared'
import { MarkdownService } from '@app/shared/renderer'
@Component({
selector: 'my-video-channel-about',

View File

@ -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>&nbsp;-&nbsp;
<a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2018</a>
<a href="https://github.com/Chocobozzz/PeerTube/blob/develop/LICENSE" title="PeerTube license" target="_blank" rel="noopener noreferrer">CopyLeft 2015-2019</a>
</footer>
</div>
</div>
</div>
<ngx-loading-bar [includeSpinner]="false"></ngx-loading-bar>
<my-confirm></my-confirm>
<p-toast position="bottom-right">
<ng-template let-message pTemplate="message">
<div class="notification-block">

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
import { Injectable } from '@angular/core'
import { environment } from '../../../environments/environment'
import { UserNotification as UserNotificationServer } from '../../../../../shared'
import { Subject } from 'rxjs'
import * as io from 'socket.io-client'
import { AuthService } from '../auth'
export type NotificationEvent = 'new' | 'read' | 'read-all'
@Injectable()
export class UserNotificationSocket {
private notificationSubject = new Subject<{ type: NotificationEvent, notification?: UserNotificationServer }>()
private socket: SocketIOClient.Socket
constructor (
private auth: AuthService
) {}
dispatch (type: NotificationEvent, notification?: UserNotificationServer) {
this.notificationSubject.next({ type, notification })
}
getMyNotificationsSocket () {
const socket = this.getSocket()
socket.on('new-notification', (n: UserNotificationServer) => this.dispatch('new', n))
return this.notificationSubject.asObservable()
}
private getSocket () {
if (this.socket) return this.socket
this.socket = io(environment.apiUrl + '/user-notifications', {
query: { accessToken: this.auth.getAccessToken() }
})
return this.socket
}
}

View File

@ -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>> = []

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
/deep/ svg {
width: inherit;
height: inherit;
}

View File

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

View File

@ -25,4 +25,6 @@
[autoClose]="true"
(onHidden)="onPopoverHidden()"
(onShown)="onPopoverShown()"
></span>
>
<my-global-icon iconName="help"></my-global-icon>
</span>

View File

@ -2,13 +2,17 @@
@import '_mixins';
.help-tooltip-button {
@include icon(17px);
position: relative;
top: -2px;
background-image: url('../../../assets/images/global/help.svg');
cursor: pointer;
border: none;
margin: 5px;
my-global-icon {
width: 17px;
position: relative;
top: -2px;
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;
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,3 @@
export * from './html-renderer.service'
export * from './linkifier.service'
export * from './markdown.service'

View File

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

View File

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

View File

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

View File

@ -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">
{{ 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 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 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 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 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">
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 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 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 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 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'">
your channel {{ notification.actorFollow.following.displayName }}
</ng-container>
<ng-container *ngIf="notification.actorFollow.following.type === 'account'">your account</ng-container>
<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 === '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>
<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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
<div [ngClass]="{ 'margin-content': marginContent }">
<div class="videos-header">
<div *ngIf="titlePage" class="title-page title-page-single">
{{ titlePage }}
<div placement="bottom" [ngbTooltip]="titleTooltip" container="body">
{{ titlePage }}
</div>
</div>
<my-feed [syndicationItems]="syndicationItems"></my-feed>
<div class="moderation-block" *ngIf="displayModerationBlock">

View File

@ -19,8 +19,8 @@
my-feed {
display: inline-block;
position: relative;
top: 1px;
min-width: 60px;
}
.moderation-block {

View File

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

View File

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

View File

@ -1,3 +1,4 @@
@import '_variables';
@import '_mixins';
.video-feed {
@ -6,14 +7,12 @@
display: block;
}
.icon {
@include icon(12px);
my-global-icon {
cursor: pointer;
width: 12px;
position: relative;
top: -2px;
&.icon-syndication {
position: relative;
top: -2px;
background-color: var(--mainForegroundColor);
mask-image: url('../../../assets/images/global/syndication.svg');
}
@include apply-svg-color(var(--mainForegroundColor))
}
}

View File

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

View File

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

View File

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

View File

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