From 2de96f4d6b800076743ed2073f9529816cfd5c8a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 30 Oct 2017 20:26:06 +0100 Subject: Lazy description and previews to video form --- client/.bootstraprc | 2 +- client/src/app/core/auth/auth.service.ts | 24 ++++---- .../src/app/shared/forms/form-validators/video.ts | 4 +- .../videos/+video-edit/video-add.component.html | 8 +-- .../app/videos/+video-edit/video-add.component.ts | 20 +++++-- .../src/app/videos/+video-edit/video-add.module.ts | 11 +--- .../app/videos/+video-edit/video-edit.module.ts | 33 +++++++++++ .../videos/+video-edit/video-update.component.html | 9 +-- .../videos/+video-edit/video-update.component.ts | 33 +++++++---- .../app/videos/+video-edit/video-update.module.ts | 11 +--- .../+video-watch/video-report.component.html | 2 +- .../videos/+video-watch/video-watch.component.html | 10 ++++ .../videos/+video-watch/video-watch.component.scss | 12 ++++ .../videos/+video-watch/video-watch.component.ts | 45 +++++++++++++- client/src/app/videos/shared/index.ts | 1 + .../videos/shared/video-description.component.html | 9 +++ .../videos/shared/video-description.component.scss | 15 +++++ .../videos/shared/video-description.component.ts | 68 ++++++++++++++++++++++ .../src/app/videos/shared/video-details.model.ts | 2 + client/src/app/videos/shared/video.service.ts | 14 ++--- client/tslint.json | 2 +- server/models/video/video-interface.ts | 2 +- server/tests/api/check-params/videos.ts | 8 +-- 23 files changed, 269 insertions(+), 76 deletions(-) create mode 100644 client/src/app/videos/+video-edit/video-edit.module.ts create mode 100644 client/src/app/videos/shared/video-description.component.html create mode 100644 client/src/app/videos/shared/video-description.component.scss create mode 100644 client/src/app/videos/shared/video-description.component.ts diff --git a/client/.bootstraprc b/client/.bootstraprc index e560cb5fb..6ceef4fe9 100644 --- a/client/.bootstraprc +++ b/client/.bootstraprc @@ -81,7 +81,7 @@ styles: dropdowns: true button-groups: true input-groups: true - navs: false + navs: true navbar: false breadcrumbs: false pagination: true diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index df6e5135b..913c857e3 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -3,6 +3,8 @@ import { Router } from '@angular/router' import { Observable } from 'rxjs/Observable' import { Subject } from 'rxjs/Subject' import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' +import { ReplaySubject } from 'rxjs/ReplaySubject' +import 'rxjs/add/operator/do' import 'rxjs/add/operator/map' import 'rxjs/add/operator/mergeMap' import 'rxjs/add/observable/throw' @@ -54,6 +56,7 @@ export class AuthService { private static BASE_USER_INFORMATION_URL = API_URL + '/api/v1/users/me' loginChangedSource: Observable + userInformationLoaded = new ReplaySubject(1) private clientId: string private clientSecret: string @@ -199,16 +202,17 @@ export class AuthService { } this.mergeUserInformation(obj) - .subscribe( - res => { - this.user.displayNSFW = res.displayNSFW - this.user.role = res.role - this.user.videoChannels = res.videoChannels - this.user.author = res.author - - this.user.save() - } - ) + .do(() => this.userInformationLoaded.next(true)) + .subscribe( + res => { + this.user.displayNSFW = res.displayNSFW + this.user.role = res.role + this.user.videoChannels = res.videoChannels + this.user.author = res.author + + this.user.save() + } + ) } private mergeUserInformation (obj: UserLoginWithUsername): Observable { diff --git a/client/src/app/shared/forms/form-validators/video.ts b/client/src/app/shared/forms/form-validators/video.ts index 286a11179..434773501 100644 --- a/client/src/app/shared/forms/form-validators/video.ts +++ b/client/src/app/shared/forms/form-validators/video.ts @@ -36,11 +36,11 @@ export const VIDEO_CHANNEL = { } export const VIDEO_DESCRIPTION = { - VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(250) ], + VALIDATORS: [ Validators.required, Validators.minLength(3), Validators.maxLength(3000) ], MESSAGES: { 'required': 'Video description is required.', 'minlength': 'Video description must be at least 3 characters long.', - 'maxlength': 'Video description cannot be more than 250 characters long.' + 'maxlength': 'Video description cannot be more than 3000 characters long.' } } diff --git a/client/src/app/videos/+video-edit/video-add.component.html b/client/src/app/videos/+video-edit/video-add.component.html index 3bf4101f4..a70788ed8 100644 --- a/client/src/app/videos/+video-edit/video-add.component.html +++ b/client/src/app/videos/+video-edit/video-add.component.html @@ -28,7 +28,6 @@
@@ -103,11 +102,8 @@
- + +
{{ formErrors.description }}
diff --git a/client/src/app/videos/+video-edit/video-add.component.ts b/client/src/app/videos/+video-edit/video-add.component.ts index 92b03e8c9..5b5557ed9 100644 --- a/client/src/app/videos/+video-edit/video-add.component.ts +++ b/client/src/app/videos/+video-edit/video-add.component.ts @@ -82,7 +82,7 @@ export class VideoAddComponent extends FormReactive implements OnInit { category: [ '', VIDEO_CATEGORY.VALIDATORS ], licence: [ '', VIDEO_LICENCE.VALIDATORS ], language: [ '', VIDEO_LANGUAGE.VALIDATORS ], - channelId: [ this.userVideoChannels[0].id, VIDEO_CHANNEL.VALIDATORS ], + channelId: [ '', VIDEO_CHANNEL.VALIDATORS ], description: [ '', VIDEO_DESCRIPTION.VALIDATORS ], videofile: [ '', VIDEO_FILE.VALIDATORS ], tags: [ '' ] @@ -96,10 +96,22 @@ export class VideoAddComponent extends FormReactive implements OnInit { this.videoLicences = this.serverService.getVideoLicences() this.videoLanguages = this.serverService.getVideoLanguages() - const user = this.authService.getUser() - this.userVideoChannels = user.videoChannels.map(v => ({ id: v.id, label: v.name })) - this.buildForm() + + this.authService.userInformationLoaded + .subscribe( + () => { + const user = this.authService.getUser() + if (!user) return + + const videoChannels = user.videoChannels + if (Array.isArray(videoChannels) === false) return + + this.userVideoChannels = videoChannels.map(v => ({ id: v.id, label: v.name })) + + this.form.patchValue({ channelId: this.userVideoChannels[0].id }) + } + ) } // The goal is to keep reactive form validation (required field) diff --git a/client/src/app/videos/+video-edit/video-add.module.ts b/client/src/app/videos/+video-edit/video-add.module.ts index 141d33ad2..3d937b008 100644 --- a/client/src/app/videos/+video-edit/video-add.module.ts +++ b/client/src/app/videos/+video-edit/video-add.module.ts @@ -1,17 +1,14 @@ import { NgModule } from '@angular/core' -import { TagInputModule } from 'ngx-chips' - import { VideoAddRoutingModule } from './video-add-routing.module' import { VideoAddComponent } from './video-add.component' -import { VideoService } from '../shared' +import { VideoEditModule } from './video-edit.module' import { SharedModule } from '../../shared' @NgModule({ imports: [ - TagInputModule, - VideoAddRoutingModule, + VideoEditModule, SharedModule ], @@ -23,8 +20,6 @@ import { SharedModule } from '../../shared' VideoAddComponent ], - providers: [ - VideoService - ] + providers: [ ] }) export class VideoAddModule { } diff --git a/client/src/app/videos/+video-edit/video-edit.module.ts b/client/src/app/videos/+video-edit/video-edit.module.ts new file mode 100644 index 000000000..33f654960 --- /dev/null +++ b/client/src/app/videos/+video-edit/video-edit.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core' + +import { TagInputModule } from 'ngx-chips' +import { TabsModule } from 'ngx-bootstrap/tabs' + +import { VideoService, MarkdownService, VideoDescriptionComponent } from '../shared' +import { SharedModule } from '../../shared' + +@NgModule({ + imports: [ + TagInputModule, + TabsModule.forRoot(), + + SharedModule + ], + + declarations: [ + VideoDescriptionComponent + ], + + exports: [ + TagInputModule, + TabsModule, + + VideoDescriptionComponent + ], + + providers: [ + VideoService, + MarkdownService + ] +}) +export class VideoEditModule { } diff --git a/client/src/app/videos/+video-edit/video-update.component.html b/client/src/app/videos/+video-edit/video-update.component.html index 4dcb3ea56..ec040630e 100644 --- a/client/src/app/videos/+video-edit/video-update.component.html +++ b/client/src/app/videos/+video-edit/video-update.component.html @@ -62,7 +62,7 @@
- (press enter to add the tag) + (press enter to add the tag) - + +
{{ formErrors.description }}
diff --git a/client/src/app/videos/+video-edit/video-update.component.ts b/client/src/app/videos/+video-edit/video-update.component.ts index 30390ac05..6ced77f1a 100644 --- a/client/src/app/videos/+video-edit/video-update.component.ts +++ b/client/src/app/videos/+video-edit/video-update.component.ts @@ -1,6 +1,8 @@ import { Component, OnInit } from '@angular/core' import { FormBuilder, FormGroup } from '@angular/forms' import { ActivatedRoute, Router } from '@angular/router' +import { Observable } from 'rxjs/Observable' +import 'rxjs/add/observable/forkJoin' import { NotificationsService } from 'angular2-notifications' @@ -84,19 +86,26 @@ export class VideoUpdateComponent extends FormReactive implements OnInit { this.videoLanguages = this.serverService.getVideoLanguages() const uuid: string = this.route.snapshot.params['uuid'] - this.videoService.getVideo(uuid) - .subscribe( - video => { - this.video = new VideoEdit(video) - - this.hydrateFormFromVideo() - }, - err => { - console.error(err) - this.error = 'Cannot fetch video.' - } - ) + this.videoService.getVideo(uuid) + .switchMap(video => { + return this.videoService + .loadCompleteDescription(video.descriptionPath) + .do(description => video.description = description) + .map(() => video) + }) + .subscribe( + video => { + this.video = new VideoEdit(video) + + this.hydrateFormFromVideo() + }, + + err => { + console.error(err) + this.error = 'Cannot fetch video.' + } + ) } checkForm () { diff --git a/client/src/app/videos/+video-edit/video-update.module.ts b/client/src/app/videos/+video-edit/video-update.module.ts index eeb2e35e2..f7bd77c75 100644 --- a/client/src/app/videos/+video-edit/video-update.module.ts +++ b/client/src/app/videos/+video-edit/video-update.module.ts @@ -1,17 +1,14 @@ import { NgModule } from '@angular/core' -import { TagInputModule } from 'ngx-chips' - import { VideoUpdateRoutingModule } from './video-update-routing.module' import { VideoUpdateComponent } from './video-update.component' -import { VideoService } from '../shared' +import { VideoEditModule } from './video-edit.module' import { SharedModule } from '../../shared' @NgModule({ imports: [ - TagInputModule, - VideoUpdateRoutingModule, + VideoEditModule, SharedModule ], @@ -23,8 +20,6 @@ import { SharedModule } from '../../shared' VideoUpdateComponent ], - providers: [ - VideoService - ] + providers: [ ] }) export class VideoUpdateModule { } diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html index 741080ead..ceb7cf50a 100644 --- a/client/src/app/videos/+video-watch/video-report.component.html +++ b/client/src/app/videos/+video-watch/video-report.component.html @@ -13,7 +13,7 @@
- + + + + + + diff --git a/client/src/app/videos/shared/video-description.component.scss b/client/src/app/videos/shared/video-description.component.scss new file mode 100644 index 000000000..d8d73e846 --- /dev/null +++ b/client/src/app/videos/shared/video-description.component.scss @@ -0,0 +1,15 @@ +textarea { + height: 150px; +} + +.previews /deep/ { + .nav { + margin-top: 10px; + font-size: 0.9em; + } + + .tab-content { + min-height: 75px; + padding: 5px; + } +} diff --git a/client/src/app/videos/shared/video-description.component.ts b/client/src/app/videos/shared/video-description.component.ts new file mode 100644 index 000000000..d9ffb7800 --- /dev/null +++ b/client/src/app/videos/shared/video-description.component.ts @@ -0,0 +1,68 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core' +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms' +import { Subject } from 'rxjs/Subject' +import 'rxjs/add/operator/debounceTime' +import 'rxjs/add/operator/distinctUntilChanged' + +import { truncate } from 'lodash' + +import { MarkdownService } from './markdown.service' + +@Component({ + selector: 'my-video-description', + templateUrl: './video-description.component.html', + styleUrls: [ './video-description.component.scss' ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => VideoDescriptionComponent), + multi: true + } + ] +}) + +export class VideoDescriptionComponent implements ControlValueAccessor, OnInit { + @Input() description = '' + truncatedDescriptionHTML = '' + descriptionHTML = '' + + private descriptionChanged = new Subject() + + constructor (private markdownService: MarkdownService) {} + + ngOnInit () { + this.descriptionChanged + .debounceTime(150) + .distinctUntilChanged() + .subscribe(() => this.updateDescriptionPreviews()) + + this.descriptionChanged.next(this.description) + } + + propagateChange = (_: any) => { /* empty */ } + + writeValue (description: string) { + this.description = description + + this.descriptionChanged.next(this.description) + } + + registerOnChange (fn: (_: any) => void) { + this.propagateChange = fn + } + + registerOnTouched () { + // Unused + } + + onModelChange () { + this.propagateChange(this.description) + + this.descriptionChanged.next(this.description) + } + + private updateDescriptionPreviews () { + this.truncatedDescriptionHTML = this.markdownService.markdownToHTML(truncate(this.description, { length: 250 })) + this.descriptionHTML = this.markdownService.markdownToHTML(this.description) + } +} diff --git a/client/src/app/videos/shared/video-details.model.ts b/client/src/app/videos/shared/video-details.model.ts index 3a6ecc480..68ded5210 100644 --- a/client/src/app/videos/shared/video-details.model.ts +++ b/client/src/app/videos/shared/video-details.model.ts @@ -38,12 +38,14 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { likes: number dislikes: number nsfw: boolean + descriptionPath: string files: VideoFile[] channel: VideoChannel constructor (hash: VideoDetailsServerModel) { super(hash) + this.descriptionPath = hash.descriptionPath this.files = hash.files this.channel = hash.channel } diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts index 8fdc1f213..7d5372334 100644 --- a/client/src/app/videos/shared/video.service.ts +++ b/client/src/app/videos/shared/video.service.ts @@ -99,15 +99,11 @@ export class VideoService { .catch((res) => this.restExtractor.handleError(res)) } - reportVideo (id: number, reason: string) { - const url = VideoService.BASE_VIDEO_URL + id + '/abuse' - const body: VideoAbuseCreate = { - reason - } - - return this.authHttp.post(url, body) - .map(this.restExtractor.extractDataBool) - .catch(res => this.restExtractor.handleError(res)) + loadCompleteDescription (descriptionPath: string) { + return this.authHttp + .get(API_URL + descriptionPath) + .map(res => res['description']) + .catch((res) => this.restExtractor.handleError(res)) } setVideoLike (id: number) { diff --git a/client/tslint.json b/client/tslint.json index 6438519a6..068fe596e 100644 --- a/client/tslint.json +++ b/client/tslint.json @@ -21,7 +21,7 @@ "no-attribute-parameter-decorator": true, "no-input-rename": true, "no-output-rename": true, - "no-forward-ref": true, + "no-forward-ref": false, "use-life-cycle-interface": true, "use-pipe-transform-interface": true, "pipe-naming": [true, "camelCase", "my"], diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 3a7bc82a4..587652f45 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -138,7 +138,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In getOriginalFileHeight: VideoMethods.GetOriginalFileHeight getEmbedPath: VideoMethods.GetEmbedPath getDescriptionPath: VideoMethods.GetDescriptionPath - getTruncatedDescription : VideoMethods.GetTruncatedDescription + getTruncatedDescription: VideoMethods.GetTruncatedDescription setTags: Sequelize.HasManySetAssociationsMixin addVideoFile: Sequelize.HasManyAddAssociationMixin diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 765b9c16e..c59f5da93 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -280,9 +280,7 @@ describe('Test videos API validator', function () { licence: 1, language: 6, nsfw: false, - description: 'my super description which is very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very long', + description: 'my super description which is very very very very very very very very very very very very very very long'.repeat(35), tags: [ 'tag1', 'tag2' ], channelId } @@ -617,9 +615,7 @@ describe('Test videos API validator', function () { licence: 2, language: 6, nsfw: false, - description: 'my super description which is very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very very very very very very very' + - 'very very very very very very very very very very very very very very very long', + description: 'my super description which is very very very very very very very very very very very very very long'.repeat(35), tags: [ 'tag1', 'tag2' ] } await makePutBodyRequest({ url: server.url, path: path + videoId, token: server.accessToken, fields }) -- cgit v1.2.3