Client: Handle NSFW video
authorChocobozzz <florian.bigard@gmail.com>
Tue, 4 Apr 2017 19:37:03 +0000 (21:37 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Tue, 4 Apr 2017 19:37:03 +0000 (21:37 +0200)
15 files changed:
client/src/app/app.component.ts
client/src/app/core/auth/auth-user.model.ts
client/src/app/core/config/config.service.ts [new file with mode: 0644]
client/src/app/core/config/index.ts [new file with mode: 0644]
client/src/app/core/core.module.ts
client/src/app/core/index.ts
client/src/app/shared/users/user.model.ts
client/src/app/videos/shared/video.model.ts
client/src/app/videos/video-add/video-add.component.html
client/src/app/videos/video-add/video-add.component.ts
client/src/app/videos/video-list/video-miniature.component.html
client/src/app/videos/video-list/video-miniature.component.scss
client/src/app/videos/video-list/video-miniature.component.ts
client/src/app/videos/video-watch/video-watch.component.ts
client/src/app/videos/video-watch/webtorrent.service.ts

index 4e33fae52b08c9ccff2618edfcdc9c0fe001527b..3c06b320e03a873b1eb94f974026e58f56f513cd 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, OnInit, ViewContainerRef } from '@angular/core';
 import { Router } from '@angular/router';
 
-import { AuthService } from './core';
+import { AuthService, ConfigService } from './core';
 import { VideoService } from './videos';
 import { UserService } from './shared';
 
@@ -27,6 +27,7 @@ export class AppComponent implements OnInit {
   constructor(
     private router: Router,
     private authService: AuthService,
+    private configService: ConfigService,
     private userService: UserService,
     private videoService: VideoService,
     viewContainerRef: ViewContainerRef
@@ -38,6 +39,7 @@ export class AppComponent implements OnInit {
       this.userService.checkTokenValidity();
     }
 
+    this.configService.loadConfig();
     this.videoService.loadVideoCategories();
     this.videoService.loadVideoLicences();
   }
index 5d61954d6105e2777d2d9d5c42ca13bb3f05920c..cb7e88d19e1f248d94dd0b33077f37d0132c705d 100644 (file)
@@ -5,7 +5,8 @@ export class AuthUser extends User {
   private static KEYS = {
     ID: 'id',
     ROLE: 'role',
-    USERNAME: 'username'
+    USERNAME: 'username',
+    DISPLAY_NSFW: 'display_nsfw'
   };
 
   tokens: Tokens;
@@ -17,7 +18,8 @@ export class AuthUser extends User {
         {
           id: parseInt(localStorage.getItem(this.KEYS.ID)),
           username: localStorage.getItem(this.KEYS.USERNAME),
-          role: localStorage.getItem(this.KEYS.ROLE)
+          role: localStorage.getItem(this.KEYS.ROLE),
+          displayNSFW: localStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true'
         },
         Tokens.load()
       );
@@ -30,10 +32,16 @@ export class AuthUser extends User {
     localStorage.removeItem(this.KEYS.USERNAME);
     localStorage.removeItem(this.KEYS.ID);
     localStorage.removeItem(this.KEYS.ROLE);
+    localStorage.removeItem(this.KEYS.DISPLAY_NSFW);
     Tokens.flush();
   }
 
-  constructor(userHash: { id: number, username: string, role: string }, hashTokens: any) {
+  constructor(userHash: {
+    id: number,
+    username: string,
+    role: string,
+    displayNSFW: boolean
+  }, hashTokens: any) {
     super(userHash);
     this.tokens = new Tokens(hashTokens);
   }
@@ -59,6 +67,7 @@ export class AuthUser extends User {
     localStorage.setItem(AuthUser.KEYS.ID, this.id.toString());
     localStorage.setItem(AuthUser.KEYS.USERNAME, this.username);
     localStorage.setItem(AuthUser.KEYS.ROLE, this.role);
+    localStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW);
     this.tokens.save();
   }
 }
diff --git a/client/src/app/core/config/config.service.ts b/client/src/app/core/config/config.service.ts
new file mode 100644 (file)
index 0000000..295e68c
--- /dev/null
@@ -0,0 +1,36 @@
+import { Injectable } from '@angular/core';
+import { Http } from '@angular/http';
+
+import { RestExtractor } from '../../shared/rest';
+
+@Injectable()
+export class ConfigService {
+  private static BASE_CONFIG_URL = '/api/v1/config/';
+
+  private config: {
+    signup: {
+      enabled: boolean
+    }
+  } = {
+    signup: {
+      enabled: false
+    }
+  };
+
+  constructor(
+    private http: Http,
+    private restExtractor: RestExtractor,
+  ) {}
+
+  loadConfig() {
+    this.http.get(ConfigService.BASE_CONFIG_URL)
+             .map(this.restExtractor.extractDataGet)
+             .subscribe(data => {
+               this.config = data;
+             });
+  }
+
+  getConfig() {
+    return this.config;
+  }
+}
diff --git a/client/src/app/core/config/index.ts b/client/src/app/core/config/index.ts
new file mode 100644 (file)
index 0000000..9039225
--- /dev/null
@@ -0,0 +1 @@
+export * from './config.service';
index ae2930552862435f90e7ae65a8a646de830c72fe..9a5ee52211ad423d52c2d4649e45b424e92a352c 100644 (file)
@@ -7,6 +7,7 @@ import { SimpleNotificationsModule } from 'angular2-notifications';
 import { ModalModule } from 'ng2-bootstrap/modal';
 
 import { AuthService } from './auth';
+import { ConfigService } from './config';
 import { ConfirmComponent, ConfirmService } from './confirm';
 import { MenuComponent, MenuAdminComponent } from './menu';
 import { throwIfAlreadyLoaded } from './module-import-guard';
@@ -37,7 +38,8 @@ import { throwIfAlreadyLoaded } from './module-import-guard';
 
   providers: [
     AuthService,
-    ConfirmService
+    ConfirmService,
+    ConfigService
   ]
 })
 export class CoreModule {
index 9b4dd1999bb69de00ea3f4758418eca056a1b305..96b28658b10c4f6e01436a285b93c718e3d977a5 100644 (file)
@@ -1,4 +1,5 @@
 export * from './auth';
+export * from './config';
 export * from './confirm';
 export * from './menu';
 export * from './core.module'
index 52d89e0049f53991d6686fcc99d68fe3537ab802..f7859f495929ae257c767c76b6f5b0953580b1ad 100644 (file)
@@ -2,12 +2,20 @@ export class User {
   id: number;
   username: string;
   role: string;
+  displayNSFW: boolean;
   createdAt: Date;
 
-  constructor(hash: { id: number, username: string, role: string, createdAt?: Date }) {
+  constructor(hash: {
+    id: number,
+    username: string,
+    role: string,
+    displayNSFW?: boolean,
+    createdAt?: Date,
+  }) {
     this.id = hash.id;
     this.username = hash.username;
     this.role = hash.role;
+    this.displayNSFW = hash.displayNSFW;
 
     if (hash.createdAt) {
       this.createdAt = hash.createdAt;
index 5ed622dce63757067c7d8c2c39ba40b675db32b2..3c588c446f332ab93ce3e4d17b617daf6b7491ef 100644 (file)
@@ -1,3 +1,5 @@
+import { User } from '../../shared';
+
 export class Video {
   author: string;
   by: string;
@@ -16,6 +18,7 @@ export class Video {
   views: number;
   likes: number;
   dislikes: number;
+  nsfw: boolean;
 
   private static createByString(author: string, podHost: string) {
     return author + '@' + podHost;
@@ -47,6 +50,7 @@ export class Video {
     views: number,
     likes: number,
     dislikes: number,
+    nsfw: boolean
   }) {
     this.author  = hash.author;
     this.createdAt = new Date(hash.createdAt);
@@ -64,11 +68,17 @@ export class Video {
     this.views = hash.views;
     this.likes = hash.likes;
     this.dislikes = hash.dislikes;
+    this.nsfw = hash.nsfw;
 
     this.by = Video.createByString(hash.author, hash.podHost);
   }
 
-  isRemovableBy(user) {
+  isRemovableBy(user: User) {
     return this.isLocal === true && user && this.author === user.username;
   }
+
+  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));
+  }
 }
index 97a3c846ac3651e03051ec817fe785f0c1976699..a3c25c14b1b2c43c18304cbc144082f10f04ca13 100644 (file)
     </div>
   </div>
 
+  <div class="form-group">
+    <label for="nsfw">NSFW</label>
+    <input
+      type="checkbox" id="nsfw"
+      formControlName="nsfw"
+    >
+  </div>
+
   <div class="form-group">
     <label for="category">Category</label>
     <select class="form-control" id="category" formControlName="category">
index 8fae233d31b911559998b5ed53a8eb2eeec35427..ea7ad2e5caffca83bf07b41e93b9dd7b9a41b815 100644 (file)
@@ -71,6 +71,7 @@ export class VideoAddComponent extends FormReactive implements OnInit {
   buildForm() {
     this.form = this.formBuilder.group({
       name: [ '', VIDEO_NAME.VALIDATORS ],
+      nsfw: [ false ],
       category: [ '', VIDEO_CATEGORY.VALIDATORS ],
       licence: [ '', VIDEO_LICENCE.VALIDATORS ],
       description: [ '', VIDEO_DESCRIPTION.VALIDATORS ],
@@ -93,12 +94,14 @@ export class VideoAddComponent extends FormReactive implements OnInit {
 
     this.uploader.onBuildItemForm = (item, form) => {
       const name = this.form.value['name'];
+      const nsfw = this.form.value['nsfw'];
       const category = this.form.value['category'];
       const licence = this.form.value['licence'];
       const description = this.form.value['description'];
 
       form.append('name', name);
       form.append('category', category);
+      form.append('nsfw', nsfw);
       form.append('licence', licence);
       form.append('description', description);
 
index b2bf35435c150ee5d4827755195a32bdeee70c92..94b8926982fcefe00dac65758940c9e64cb88223 100644 (file)
@@ -3,7 +3,11 @@
     [routerLink]="['/videos/watch', video.id]" [attr.title]="video.description"
     class="video-miniature-thumbnail"
   >
-    <img [attr.src]="video.thumbnailPath" alt="video thumbnail" />
+    <img *ngIf="isVideoNSFWForThisUser() === false" [attr.src]="video.thumbnailPath" alt="video thumbnail" />
+    <div *ngIf="isVideoNSFWForThisUser()" class="thumbnail-nsfw">
+      NSFW
+    </div>
+
     <span class="video-miniature-duration">{{ video.duration }}</span>
   </a>
   <span
@@ -13,7 +17,7 @@
 
   <div class="video-miniature-informations">
     <span class="video-miniature-name-tags">
-      <a [routerLink]="['/videos/watch', video.id]" [attr.title]="video.name" class="video-miniature-name">{{ video.name }}</a>
+      <a [routerLink]="['/videos/watch', video.id]" [attr.title]="getVideoName()" class="video-miniature-name">{{ getVideoName() }}</a>
 
       <div class="video-miniature-tags">
         <span *ngFor="let tag of video.tags" class="video-miniature-tag">
index b5d24271a821b4045cb428f444881cb884f14339..b8e90e8c5eeff5cb1d68d0d3d290f217731509b8 100644 (file)
     display: inline-block;
     position: relative;
 
+    &:hover {
+      text-decoration: none !important;
+    }
+
+    .thumbnail-nsfw {
+      background-color: #000;
+      color: #fff;
+      text-align: center;
+      font-size: 30px;
+      line-height: 110px;
+
+      width: 200px;
+      height: 110px;
+    }
+
     .video-miniature-duration {
       position: absolute;
       right: 5px;
index ba47155972a08d2b24cce7290b488b9da3a2342d..888026ddebb373ff3df74b54b1774667365daf19 100644 (file)
@@ -2,7 +2,7 @@ import { Component, Input, Output, EventEmitter } from '@angular/core';
 
 import { NotificationsService } from 'angular2-notifications';
 
-import { ConfirmService } from '../../core';
+import { ConfirmService, ConfigService } from '../../core';
 import { SortField, Video, VideoService } from '../shared';
 import { User } from '../../shared';
 
@@ -24,6 +24,7 @@ export class VideoMiniatureComponent {
   constructor(
     private notificationsService: NotificationsService,
     private confirmService: ConfirmService,
+    private configService: ConfigService,
     private videoService: VideoService
   ) {}
 
@@ -31,6 +32,13 @@ export class VideoMiniatureComponent {
     return this.hovering && this.video.isRemovableBy(this.user);
   }
 
+  getVideoName() {
+    if (this.isVideoNSFWForThisUser())
+      return 'NSFW';
+
+    return this.video.name;
+  }
+
   onBlur() {
     this.hovering = false;
   }
@@ -52,4 +60,8 @@ export class VideoMiniatureComponent {
       }
     );
   }
+
+  isVideoNSFWForThisUser() {
+    return this.video.isVideoNSFWForUser(this.user);
+  }
 }
index 5678f6df82dcca01d604cc8a3c7c3251dc7fa4d8..37ed70a99f3f62017cf0a9c2b75a9ec1522ec52d 100644 (file)
@@ -1,12 +1,13 @@
 import { Component, ElementRef, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
+import { Observable } from 'rxjs/Observable';
 import { Subscription } from 'rxjs/Subscription';
 
 import * as videojs from 'video.js';
 import { MetaService } from '@nglibs/meta';
 import { NotificationsService } from 'angular2-notifications';
 
-import { AuthService } from '../../core';
+import { AuthService, ConfirmService } from '../../core';
 import { VideoMagnetComponent } from './video-magnet.component';
 import { VideoShareComponent } from './video-share.component';
 import { VideoReportComponent } from './video-report.component';
@@ -47,7 +48,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     private elementRef: ElementRef,
     private ngZone: NgZone,
     private route: ActivatedRoute,
+    private router: Router,
     private videoService: VideoService,
+    private confirmService: ConfirmService,
     private metaService: MetaService,
     private webTorrentService: WebTorrentService,
     private authService: AuthService,
@@ -58,15 +61,9 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     this.paramsSub = this.route.params.subscribe(routeParams => {
       let id = routeParams['id'];
       this.videoService.getVideo(id).subscribe(
-        video => {
-          this.video = video;
-          this.setOpenGraphTags();
-          this.loadVideo();
-          this.checkUserRating();
-        },
-        error => {
-          this.videoNotFound = true;
-        }
+        video => this.onVideoFetched(video),
+
+        error => this.videoNotFound = true
       );
     });
 
@@ -92,7 +89,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
     window.clearInterval(this.torrentInfosInterval);
     window.clearTimeout(this.errorTimer);
 
-    if (this.video !== null) {
+    if (this.video !== null && this.webTorrentService.has(this.video.magnetUri)) {
       this.webTorrentService.remove(this.video.magnetUri);
     }
 
@@ -206,6 +203,29 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
                       );
   }
 
+  private onVideoFetched(video: Video) {
+    this.video = video;
+
+    let observable;
+    if (this.video.isVideoNSFWForUser(this.authService.getUser())) {
+      observable = this.confirmService.confirm('This video is not safe for work. Are you sure you want to watch it?', 'NSFW');
+    } else {
+      observable = Observable.of(true);
+    }
+
+    observable.subscribe(
+      res => {
+        if (res === false) {
+          return this.router.navigate([ '/videos/list' ]);
+        }
+
+        this.setOpenGraphTags();
+        this.loadVideo();
+        this.checkUserRating();
+      }
+    );
+  }
+
   private updateVideoRating(oldRating: RateType, newRating: RateType) {
     let likesToIncrement = 0;
     let dislikesToIncrement = 0;
index 0192167ee88405922fa06f4baace204139253105..630a5c46983167811426081a145a56d3cc41ddba 100644 (file)
@@ -26,4 +26,8 @@ export class WebTorrentService {
   remove(magnetUri: string) {
     return this.client.remove(magnetUri);
   }
+
+  has(magnetUri: string) {
+    return this.client.get(magnetUri) !== null;
+  }
 }