diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5a484fbbd..3c8e6413c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -146,3 +146,9 @@ Build the application and run the unit/integration tests: $ npm run build $ npm test ``` + +If you just want to run 1 test: + +``` +$ npm run mocha -- --exit --require ts-node/register/type-check --bail server/tests/api/index.ts +``` diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 714a3af15..df40bba9f 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html @@ -62,6 +62,22 @@ +
+ + + +
+ +
+
+ {{ formErrors.instanceDefaultNSFWPolicy }} +
+
+
Cache
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index d73ee71e4..2ab371cbb 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -48,6 +48,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { instanceDescription: '', instanceTerms: '', instanceDefaultClientRoute: '', + instanceDefaultNSFWPolicy: '', cachePreviewsSize: '', signupLimit: '', adminEmail: '', @@ -90,6 +91,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { instanceDescription: [ '' ], instanceTerms: [ '' ], instanceDefaultClientRoute: [ '' ], + instanceDefaultNSFWPolicy: [ '' ], cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ], signupEnabled: [ ], signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ], @@ -167,6 +169,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { description: this.form.value['instanceDescription'], terms: this.form.value['instanceTerms'], defaultClientRoute: this.form.value['instanceDefaultClientRoute'], + defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'], customizations: { javascript: this.form.value['customizationJavascript'], css: this.form.value['customizationCSS'] @@ -224,6 +227,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { instanceDescription: this.customConfig.instance.description, instanceTerms: this.customConfig.instance.terms, instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, + instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy, cachePreviewsSize: this.customConfig.cache.previews.size, signupEnabled: this.customConfig.signup.enabled, signupLimit: this.customConfig.signup.limit, diff --git a/client/src/app/account/account-settings/account-details/account-details.component.html b/client/src/app/account/account-settings/account-details/account-details.component.html index 8f1475a4d..0e8598e9e 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.html +++ b/client/src/app/account/account-settings/account-details/account-details.component.html @@ -1,11 +1,15 @@
- - - + + + +
+ +
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.scss b/client/src/app/account/account-settings/account-details/account-details.component.scss index 4e8dfde1d..ed59e4689 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.scss +++ b/client/src/app/account/account-settings/account-details/account-details.component.scss @@ -12,3 +12,9 @@ input[type=submit] { display: block; margin-top: 15px; } + +.peertube-select-container { + @include peertube-select-container(340px); + + margin-bottom: 30px; +} \ No newline at end of file diff --git a/client/src/app/account/account-settings/account-details/account-details.component.ts b/client/src/app/account/account-settings/account-details/account-details.component.ts index 917f31651..de213717e 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.ts +++ b/client/src/app/account/account-settings/account-details/account-details.component.ts @@ -29,7 +29,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit { buildForm () { this.form = this.formBuilder.group({ - displayNSFW: [ this.user.displayNSFW ], + nsfwPolicy: [ this.user.nsfwPolicy ], autoPlayVideo: [ this.user.autoPlayVideo ] }) @@ -41,10 +41,10 @@ export class AccountDetailsComponent extends FormReactive implements OnInit { } updateDetails () { - const displayNSFW = this.form.value['displayNSFW'] + const nsfwPolicy = this.form.value['nsfwPolicy'] const autoPlayVideo = this.form.value['autoPlayVideo'] const details: UserUpdateMe = { - displayNSFW, + nsfwPolicy, autoPlayVideo } diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html index d7e2230b0..66ce3a77b 100644 --- a/client/src/app/account/account-videos/account-videos.component.html +++ b/client/src/app/account/account-videos/account-videos.component.html @@ -18,6 +18,7 @@
{{ video.name }} {{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views +
{{ video.privacy.label }}
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss index 449cc6af4..f276ea389 100644 --- a/client/src/app/account/account-videos/account-videos.component.scss +++ b/client/src/app/account/account-videos/account-videos.component.scss @@ -79,8 +79,12 @@ font-weight: $font-semibold; } - .video-info-date-views { + .video-info-date-views, .video-info-private { font-size: 13px; + + &.video-info-private { + font-weight: $font-semibold; + } } } diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 366eea110..74ed1c580 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts @@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum' // Do not use the barrel (dependency loop) import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' import { User, UserConstructorHash } from '../../shared/users/user.model' +import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' export type TokenOptions = { accessToken: string @@ -70,7 +71,7 @@ export class AuthUser extends User { ROLE: 'role', EMAIL: 'email', USERNAME: 'username', - DISPLAY_NSFW: 'display_nsfw', + NSFW_POLICY: 'nsfw_policy', AUTO_PLAY_VIDEO: 'auto_play_video' } @@ -85,7 +86,7 @@ export class AuthUser extends User { username: peertubeLocalStorage.getItem(this.KEYS.USERNAME), email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, - displayNSFW: peertubeLocalStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true', + nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' }, Tokens.load() @@ -99,7 +100,7 @@ export class AuthUser extends User { peertubeLocalStorage.removeItem(this.KEYS.USERNAME) peertubeLocalStorage.removeItem(this.KEYS.ID) peertubeLocalStorage.removeItem(this.KEYS.ROLE) - peertubeLocalStorage.removeItem(this.KEYS.DISPLAY_NSFW) + peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) peertubeLocalStorage.removeItem(this.KEYS.EMAIL) Tokens.flush() @@ -136,7 +137,7 @@ export class AuthUser extends User { peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username) peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) - peertubeLocalStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW)) + peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) this.tokens.save() } diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 987d64d2a..a8beb242d 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts @@ -5,7 +5,6 @@ import 'rxjs/add/operator/do' import { ReplaySubject } from 'rxjs/ReplaySubject' import { ServerConfig } from '../../../../../shared' import { About } from '../../../../../shared/models/server/about.model' -import { ServerStats } from '../../../../../shared/models/server/server-stats.model' import { environment } from '../../../environments/environment' @Injectable() @@ -26,6 +25,7 @@ export class ServerService { shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', defaultClientRoute: '', + defaultNSFWPolicy: 'do_not_list' as 'do_not_list', customizations: { javascript: '', css: '' diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index e37d93b62..3da5701a0 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html @@ -13,6 +13,9 @@ diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index b8bf3a7a5..0df8b86fa 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss @@ -12,20 +12,16 @@ } /deep/ { - .help-tooltip { - opacity: 1 !important; + .popover-body { + text-align: left; + padding: 10px; + max-width: 300px; - .tooltip-inner { - text-align: left; - padding: 10px; - max-width: 300px; - - font-size: 13px; - font-family: $main-fonts; - background-color: #fff; - color: #000; - box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); - } + font-size: 13px; + font-family: $main-fonts; + background-color: #fff; + color: #000; + box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); ul { padding-left: 20px; diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts index 89dd1dae5..0373a63de 100644 --- a/client/src/app/shared/misc/help.component.ts +++ b/client/src/app/shared/misc/help.component.ts @@ -1,6 +1,5 @@ -import { Component, ElementRef, HostListener, Input, OnInit, ViewChild, OnChanges } from '@angular/core' +import { Component, Input, OnChanges, OnInit } from '@angular/core' import { MarkdownService } from '@app/videos/shared' -import { TooltipDirective } from 'ngx-bootstrap/tooltip' @Component({ selector: 'my-help', @@ -9,16 +8,14 @@ import { TooltipDirective } from 'ngx-bootstrap/tooltip' }) export class HelpComponent implements OnInit, OnChanges { - @ViewChild('tooltipDirective') tooltipDirective: TooltipDirective @Input() preHtml = '' @Input() postHtml = '' @Input() customHtml = '' @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' + @Input() tooltipPlacement = 'right' mainHtml = '' - constructor (private elementRef: ElementRef) { } - ngOnInit () { this.init() } @@ -27,15 +24,6 @@ export class HelpComponent implements OnInit, OnChanges { this.init() } - @HostListener('document:click', ['$event.target']) - public onClick (targetElement) { - const clickedInside = this.elementRef.nativeElement.contains(targetElement) - - if (this.tooltipDirective.isOpen && !clickedInside) { - this.tooltipDirective.hide() - } - } - private init () { if (this.helpType === 'custom') { this.mainHtml = this.customHtml diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 4a94b032d..2bdc48a1d 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -1,5 +1,6 @@ import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' import { Account } from '../account/account.model' +import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' export type UserConstructorHash = { id: number, @@ -7,7 +8,7 @@ export type UserConstructorHash = { email: string, role: UserRole, videoQuota?: number, - displayNSFW?: boolean, + nsfwPolicy?: NSFWPolicyType, autoPlayVideo?: boolean, createdAt?: Date, account?: Account, @@ -18,7 +19,7 @@ export class User implements UserServerModel { username: string email: string role: UserRole - displayNSFW: boolean + nsfwPolicy: NSFWPolicyType autoPlayVideo: boolean videoQuota: number account: Account @@ -40,8 +41,8 @@ export class User implements UserServerModel { this.videoQuota = hash.videoQuota } - if (hash.displayNSFW !== undefined) { - this.displayNSFW = hash.displayNSFW + if (hash.nsfwPolicy !== undefined) { + this.nsfwPolicy = hash.nsfwPolicy } if (hash.autoPlayVideo !== undefined) { diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index a1f7207a2..5397aa37f 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts @@ -1,17 +1,9 @@ -import { - UserRight, - VideoChannel, - VideoDetails as VideoDetailsServerModel, - VideoFile, - VideoPrivacy -} from '../../../../../shared' +import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared' import { Account } from '../../../../../shared/models/actors' -import { VideoConstant } from '../../../../../shared/models/videos/video.model' import { AuthUser } from '../../core' import { Video } from '../../shared/video/video.model' export class VideoDetails extends Video implements VideoDetailsServerModel { - privacy: VideoConstant descriptionPath: string support: string channel: VideoChannel @@ -26,7 +18,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { constructor (hash: VideoDetailsServerModel) { super(hash) - this.privacy = hash.privacy this.descriptionPath = hash.descriptionPath this.files = hash.files this.channel = hash.channel diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index f28e9b8d9..233432142 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html @@ -1,11 +1,11 @@
- +
{{ video.name }} diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index 4d79a74bb..d3f6dc1f6 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts @@ -1,6 +1,7 @@ import { Component, Input } from '@angular/core' import { User } from '../users' import { Video } from './video.model' +import { ServerService } from '@app/core' @Component({ selector: 'my-video-miniature', @@ -11,7 +12,9 @@ export class VideoMiniatureComponent { @Input() user: User @Input() video: Video - isVideoNSFWForThisUser () { - return this.video.isVideoNSFWForUser(this.user) + constructor (private serverService: ServerService) { } + + isVideoBlur () { + return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) } } diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 0c02cbcb9..e25c172e0 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts @@ -1,9 +1,10 @@ import { Account } from '@app/shared/account/account.model' import { User } from '../' -import { Video as VideoServerModel } from '../../../../../shared' +import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared' import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { VideoConstant } from '../../../../../shared/models/videos/video.model' import { getAbsoluteAPIUrl } from '../misc/utils' +import { ServerConfig } from '../../../../../shared/models' export class Video implements VideoServerModel { by: string @@ -13,6 +14,7 @@ export class Video implements VideoServerModel { category: VideoConstant licence: VideoConstant language: VideoConstant + privacy: VideoConstant description: string duration: number durationLabel: string @@ -61,6 +63,7 @@ export class Video implements VideoServerModel { this.category = hash.category this.licence = hash.licence this.language = hash.language + this.privacy = hash.privacy this.description = hash.description this.duration = hash.duration this.durationLabel = Video.createDurationString(hash.duration) @@ -83,8 +86,14 @@ export class Video implements VideoServerModel { this.by = Account.CREATE_BY_STRING(hash.account.name, hash.account.host) } - isVideoNSFWForUser (user: User) { - // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos... - return (this.nsfw && (!user || user.displayNSFW === false)) + isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { + // Video is not NSFW, skip + if (this.nsfw === false) return false + + // Return user setting if logged in + if (user) return user.nsfwPolicy !== 'display' + + // Return default instance config + return serverConfig.instance.defaultNSFWPolicy !== 'display' } } diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 6d0a1600a..9cd3454a0 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html @@ -100,6 +100,7 @@ +
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 1317f7426..cf64ff589 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss @@ -9,6 +9,10 @@ @include peertube-select-disabled-container(auto); } +.form-group-checkbox { + my-help { margin-left: 5px } +} + .video-edit { height: 100%; diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 182703cdf..6f6f02378 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -22,6 +22,7 @@ import { VideoDownloadComponent } from './modal/video-download.component' import { VideoReportComponent } from './modal/video-report.component' import { VideoShareComponent } from './modal/video-share.component' import { getVideojsOptions } from '../../../assets/player/peertube-player' +import { ServerService } from '@app/core' @Component({ selector: 'my-video-watch', @@ -66,6 +67,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { private confirmService: ConfirmService, private metaService: MetaService, private authService: AuthService, + private serverService: ServerService, private notificationsService: NotificationsService, private markdownService: MarkdownService, private zone: NgZone, @@ -335,7 +337,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { this.updateOtherVideosDisplayed() - if (this.video.isVideoNSFWForUser(this.user)) { + if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { const res = await this.confirmService.confirm( 'This video contains mature or explicit content. Are you sure you want to watch it?', 'Mature or explicit content' diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index a359255da..b60b03a22 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html @@ -11,6 +11,12 @@ +
+

Sorry

+ +
+
+ diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index fc7135d64..37c1df6c4 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss @@ -4,6 +4,16 @@ @import '~videojs-dock/dist/videojs-dock.css'; @import '../../sass/video-js-custom.scss'; +[hidden] { + display: none !important; +} + +body { + font-family: $main-fonts; + font-weight: $font-regular; + color: #000; +} + video { width: 99%; } @@ -43,3 +53,38 @@ html, body { } } } + +#error-block { + display: none; + + flex-direction: column; + align-content: center; + justify-content: center; + text-align: center; + background-color: #141313; + width: 100%; + height: 100%; + color: white; + box-sizing: border-box; + font-family: sans-serif; + + #error-title { + font-size: 45px; + margin-bottom: 5px; + } + + #error-content { + font-size: 24px; + } +} + +@media screen and (max-width: 300px) { + #error-block { + font-size: 36px; + + #error-content { + font-size: 14px; + } + } +} + diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index a99bc586f..aa418d2d4 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -9,19 +9,53 @@ function getVideoUrl (id: string) { return window.location.origin + '/api/v1/videos/' + id } -async function loadVideoInfo (videoId: string): Promise { - const response = await fetch(getVideoUrl(videoId)) - return response.json() +function loadVideoInfo (videoId: string): Promise { + return fetch(getVideoUrl(videoId)) +} + +function removeElement (element: HTMLElement) { + element.parentElement.removeChild(element) +} + +function displayError (videoElement: HTMLVideoElement, text: string) { + // Remove video element + removeElement(videoElement) + + document.title = 'Sorry - ' + text + + const errorBlock = document.getElementById('error-block') + errorBlock.style.display = 'flex' + + const errorText = document.getElementById('error-content') + errorText.innerHTML = text +} + +function videoNotFound (videoElement: HTMLVideoElement) { + const text = 'This video does not exist.' + displayError(videoElement, text) +} + +function videoFetchError (videoElement: HTMLVideoElement) { + const text = 'We cannot fetch the video. Please try again later.' + displayError(videoElement, text) } const urlParts = window.location.href.split('/') const videoId = urlParts[urlParts.length - 1] loadVideoInfo(videoId) - .then(videoInfo => { + .then(async response => { const videoContainerId = 'video-container' - const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement + + if (!response.ok) { + if (response.status === 404) return videoNotFound(videoElement) + + return videoFetchError(videoElement) + } + + const videoInfo: VideoDetails = await response.json() + let autoplay = false let startTime = 0 diff --git a/config/default.yaml b/config/default.yaml index 9f4a76621..25dde72c9 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -84,6 +84,9 @@ instance: description: 'Welcome to this PeerTube instance!' # Support markdown terms: 'No terms for now.' # Support markdown default_client_route: '/videos/trending' + # By default, "do_not_list" or "blur" or "display" NSFW videos + # Could be overridden per user with a setting + default_nsfw_policy: 'do_not_list' customizations: javascript: '' # Directly your JavaScript code (without