diff options
Diffstat (limited to 'client/src/app')
31 files changed, 579 insertions, 35 deletions
diff --git a/client/src/app/shared/forms/form-validators/video-comment.ts b/client/src/app/shared/forms/form-validators/video-comment.ts new file mode 100644 index 000000000..42a97e300 --- /dev/null +++ b/client/src/app/shared/forms/form-validators/video-comment.ts | |||
@@ -0,0 +1,10 @@ | |||
1 | import { Validators } from '@angular/forms' | ||
2 | |||
3 | export const VIDEO_COMMENT_TEXT = { | ||
4 | VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], | ||
5 | MESSAGES: { | ||
6 | 'required': 'Comment is required.', | ||
7 | 'minlength': 'Comment must be at least 2 characters long.', | ||
8 | 'maxlength': 'Comment cannot be more than 3000 characters long.' | ||
9 | } | ||
10 | } | ||
diff --git a/client/src/app/shared/misc/button.component.scss b/client/src/app/shared/misc/button.component.scss index c380c7ae1..145a3474a 100644 --- a/client/src/app/shared/misc/button.component.scss +++ b/client/src/app/shared/misc/button.component.scss | |||
@@ -20,7 +20,7 @@ | |||
20 | top: -2px; | 20 | top: -2px; |
21 | 21 | ||
22 | &.icon-edit { | 22 | &.icon-edit { |
23 | background-image: url('../../../assets/images/global/edit.svg'); | 23 | background-image: url('../../../assets/images/global/edit-grey.svg'); |
24 | } | 24 | } |
25 | 25 | ||
26 | &.icon-delete-grey { | 26 | &.icon-delete-grey { |
diff --git a/client/src/app/shared/video/video-pagination.model.ts b/client/src/app/shared/rest/component-pagination.model.ts index e9db61596..0b8ecc318 100644 --- a/client/src/app/shared/video/video-pagination.model.ts +++ b/client/src/app/shared/rest/component-pagination.model.ts | |||
@@ -1,4 +1,4 @@ | |||
1 | export interface VideoPagination { | 1 | export interface ComponentPagination { |
2 | currentPage: number | 2 | currentPage: number |
3 | itemsPerPage: number | 3 | itemsPerPage: number |
4 | totalItems?: number | 4 | totalItems?: number |
diff --git a/client/src/app/shared/rest/rest.service.ts b/client/src/app/shared/rest/rest.service.ts index a1c301050..5d5410de9 100644 --- a/client/src/app/shared/rest/rest.service.ts +++ b/client/src/app/shared/rest/rest.service.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Injectable } from '@angular/core' | 1 | import { Injectable } from '@angular/core' |
2 | import { HttpParams } from '@angular/common/http' | 2 | import { HttpParams } from '@angular/common/http' |
3 | import { SortMeta } from 'primeng/components/common/sortmeta' | 3 | import { SortMeta } from 'primeng/components/common/sortmeta' |
4 | import { ComponentPagination } from './component-pagination.model' | ||
4 | 5 | ||
5 | import { RestPagination } from './rest-pagination' | 6 | import { RestPagination } from './rest-pagination' |
6 | 7 | ||
@@ -31,4 +32,10 @@ export class RestService { | |||
31 | return newParams | 32 | return newParams |
32 | } | 33 | } |
33 | 34 | ||
35 | componentPaginationToRestPagination (componentPagination: ComponentPagination): RestPagination { | ||
36 | const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage | ||
37 | const count: number = componentPagination.itemsPerPage | ||
38 | |||
39 | return { start, count } | ||
40 | } | ||
34 | } | 41 | } |
diff --git a/client/src/app/shared/video/abstract-video-list.ts b/client/src/app/shared/video/abstract-video-list.ts index 2b6870a78..bfe46bcdd 100644 --- a/client/src/app/shared/video/abstract-video-list.ts +++ b/client/src/app/shared/video/abstract-video-list.ts | |||
@@ -3,12 +3,12 @@ import { ActivatedRoute, Router } from '@angular/router' | |||
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | import { Observable } from 'rxjs/Observable' | 4 | import { Observable } from 'rxjs/Observable' |
5 | import { AuthService } from '../../core/auth' | 5 | import { AuthService } from '../../core/auth' |
6 | import { ComponentPagination } from '../rest/component-pagination.model' | ||
6 | import { SortField } from './sort-field.type' | 7 | import { SortField } from './sort-field.type' |
7 | import { VideoPagination } from './video-pagination.model' | ||
8 | import { Video } from './video.model' | 8 | import { Video } from './video.model' |
9 | 9 | ||
10 | export abstract class AbstractVideoList implements OnInit { | 10 | export abstract class AbstractVideoList implements OnInit { |
11 | pagination: VideoPagination = { | 11 | pagination: ComponentPagination = { |
12 | currentPage: 1, | 12 | currentPage: 1, |
13 | itemsPerPage: 25, | 13 | itemsPerPage: 25, |
14 | totalItems: null | 14 | totalItems: null |
diff --git a/client/src/app/shared/video/video-miniature.component.scss b/client/src/app/shared/video/video-miniature.component.scss index 49ba1e51c..f0888ad9f 100644 --- a/client/src/app/shared/video/video-miniature.component.scss +++ b/client/src/app/shared/video/video-miniature.component.scss | |||
@@ -18,7 +18,6 @@ | |||
18 | overflow: hidden; | 18 | overflow: hidden; |
19 | text-overflow: ellipsis; | 19 | text-overflow: ellipsis; |
20 | white-space: nowrap; | 20 | white-space: nowrap; |
21 | font-weight: bold; | ||
22 | transition: color 0.2s; | 21 | transition: color 0.2s; |
23 | font-size: 16px; | 22 | font-size: 16px; |
24 | font-weight: $font-semibold; | 23 | font-weight: $font-semibold; |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 91dd3977a..fc7505a51 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -10,13 +10,13 @@ import { UserVideoRate } from '../../../../../shared/models/videos/user-video-ra | |||
10 | import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' | 10 | import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' |
11 | import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' | 11 | import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' |
12 | import { environment } from '../../../environments/environment' | 12 | import { environment } from '../../../environments/environment' |
13 | import { ComponentPagination } from '../rest/component-pagination.model' | ||
13 | import { RestExtractor } from '../rest/rest-extractor.service' | 14 | import { RestExtractor } from '../rest/rest-extractor.service' |
14 | import { RestService } from '../rest/rest.service' | 15 | import { RestService } from '../rest/rest.service' |
15 | import { UserService } from '../users/user.service' | 16 | import { UserService } from '../users/user.service' |
16 | import { SortField } from './sort-field.type' | 17 | import { SortField } from './sort-field.type' |
17 | import { VideoDetails } from './video-details.model' | 18 | import { VideoDetails } from './video-details.model' |
18 | import { VideoEdit } from './video-edit.model' | 19 | import { VideoEdit } from './video-edit.model' |
19 | import { VideoPagination } from './video-pagination.model' | ||
20 | import { Video } from './video.model' | 20 | import { Video } from './video.model' |
21 | 21 | ||
22 | @Injectable() | 22 | @Injectable() |
@@ -71,8 +71,8 @@ export class VideoService { | |||
71 | .catch(this.restExtractor.handleError) | 71 | .catch(this.restExtractor.handleError) |
72 | } | 72 | } |
73 | 73 | ||
74 | getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | 74 | getMyVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { |
75 | const pagination = this.videoPaginationToRestPagination(videoPagination) | 75 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) |
76 | 76 | ||
77 | let params = new HttpParams() | 77 | let params = new HttpParams() |
78 | params = this.restService.addRestGetParams(params, pagination, sort) | 78 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -82,8 +82,8 @@ export class VideoService { | |||
82 | .catch((res) => this.restExtractor.handleError(res)) | 82 | .catch((res) => this.restExtractor.handleError(res)) |
83 | } | 83 | } |
84 | 84 | ||
85 | getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | 85 | getVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { |
86 | const pagination = this.videoPaginationToRestPagination(videoPagination) | 86 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) |
87 | 87 | ||
88 | let params = new HttpParams() | 88 | let params = new HttpParams() |
89 | params = this.restService.addRestGetParams(params, pagination, sort) | 89 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -94,10 +94,14 @@ export class VideoService { | |||
94 | .catch((res) => this.restExtractor.handleError(res)) | 94 | .catch((res) => this.restExtractor.handleError(res)) |
95 | } | 95 | } |
96 | 96 | ||
97 | searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { | 97 | searchVideos ( |
98 | search: string, | ||
99 | videoPagination: ComponentPagination, | ||
100 | sort: SortField | ||
101 | ): Observable<{ videos: Video[], totalVideos: number}> { | ||
98 | const url = VideoService.BASE_VIDEO_URL + 'search' | 102 | const url = VideoService.BASE_VIDEO_URL + 'search' |
99 | 103 | ||
100 | const pagination = this.videoPaginationToRestPagination(videoPagination) | 104 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) |
101 | 105 | ||
102 | let params = new HttpParams() | 106 | let params = new HttpParams() |
103 | params = this.restService.addRestGetParams(params, pagination, sort) | 107 | params = this.restService.addRestGetParams(params, pagination, sort) |
@@ -139,13 +143,6 @@ export class VideoService { | |||
139 | .catch(res => this.restExtractor.handleError(res)) | 143 | .catch(res => this.restExtractor.handleError(res)) |
140 | } | 144 | } |
141 | 145 | ||
142 | private videoPaginationToRestPagination (videoPagination: VideoPagination) { | ||
143 | const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage | ||
144 | const count: number = videoPagination.itemsPerPage | ||
145 | |||
146 | return { start, count } | ||
147 | } | ||
148 | |||
149 | private setVideoRate (id: number, rateType: VideoRateType) { | 146 | private setVideoRate (id: number, rateType: VideoRateType) { |
150 | const url = VideoService.BASE_VIDEO_URL + id + '/rate' | 147 | const url = VideoService.BASE_VIDEO_URL + id + '/rate' |
151 | const body: UserVideoRateUpdate = { | 148 | const body: UserVideoRateUpdate = { |
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.html b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html new file mode 100644 index 000000000..792053614 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.html | |||
@@ -0,0 +1,15 @@ | |||
1 | <form novalidate [formGroup]="form" (ngSubmit)="formValidated()"> | ||
2 | <div class="form-group"> | ||
3 | <textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"> | ||
4 | </textarea> | ||
5 | <div *ngIf="formErrors.text" class="form-error"> | ||
6 | {{ formErrors.text }} | ||
7 | </div> | ||
8 | </div> | ||
9 | |||
10 | <div class="submit-comment"> | ||
11 | <button *ngIf="isAddButtonDisplayed()" [ngClass]="{ disabled: !form.valid }"> | ||
12 | Post comment | ||
13 | </button> | ||
14 | </div> | ||
15 | </form> | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss new file mode 100644 index 000000000..9661062e8 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.scss | |||
@@ -0,0 +1,20 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .form-group { | ||
5 | margin-bottom: 10px; | ||
6 | } | ||
7 | |||
8 | textarea { | ||
9 | @include peertube-textarea(100%, 150px); | ||
10 | } | ||
11 | |||
12 | .submit-comment { | ||
13 | display: flex; | ||
14 | justify-content: end; | ||
15 | |||
16 | button { | ||
17 | @include peertube-button; | ||
18 | @include orange-button | ||
19 | } | ||
20 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts new file mode 100644 index 000000000..5ad83fc47 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment-add.component.ts | |||
@@ -0,0 +1,84 @@ | |||
1 | import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||
2 | import { FormBuilder, FormGroup } from '@angular/forms' | ||
3 | import { NotificationsService } from 'angular2-notifications' | ||
4 | import { Observable } from 'rxjs/Observable' | ||
5 | import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' | ||
6 | import { FormReactive } from '../../../shared' | ||
7 | import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment' | ||
8 | import { Video } from '../../../shared/video/video.model' | ||
9 | import { VideoComment } from './video-comment.model' | ||
10 | import { VideoCommentService } from './video-comment.service' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-video-comment-add', | ||
14 | templateUrl: './video-comment-add.component.html', | ||
15 | styleUrls: ['./video-comment-add.component.scss'] | ||
16 | }) | ||
17 | export class VideoCommentAddComponent extends FormReactive implements OnInit { | ||
18 | @Input() video: Video | ||
19 | @Input() parentComment: VideoComment | ||
20 | |||
21 | @Output() commentCreated = new EventEmitter<VideoCommentCreate>() | ||
22 | |||
23 | form: FormGroup | ||
24 | formErrors = { | ||
25 | 'text': '' | ||
26 | } | ||
27 | validationMessages = { | ||
28 | 'text': VIDEO_COMMENT_TEXT.MESSAGES | ||
29 | } | ||
30 | |||
31 | constructor ( | ||
32 | private formBuilder: FormBuilder, | ||
33 | private notificationsService: NotificationsService, | ||
34 | private videoCommentService: VideoCommentService | ||
35 | ) { | ||
36 | super() | ||
37 | } | ||
38 | |||
39 | buildForm () { | ||
40 | this.form = this.formBuilder.group({ | ||
41 | text: [ '', VIDEO_COMMENT_TEXT.VALIDATORS ] | ||
42 | }) | ||
43 | |||
44 | this.form.valueChanges.subscribe(data => this.onValueChanged(data)) | ||
45 | } | ||
46 | |||
47 | ngOnInit () { | ||
48 | this.buildForm() | ||
49 | } | ||
50 | |||
51 | formValidated () { | ||
52 | const commentCreate: VideoCommentCreate = this.form.value | ||
53 | let obs: Observable<any> | ||
54 | |||
55 | if (this.parentComment) { | ||
56 | obs = this.addCommentReply(commentCreate) | ||
57 | } else { | ||
58 | obs = this.addCommentThread(commentCreate) | ||
59 | } | ||
60 | |||
61 | obs.subscribe( | ||
62 | comment => { | ||
63 | this.commentCreated.emit(comment) | ||
64 | this.form.reset() | ||
65 | }, | ||
66 | |||
67 | err => this.notificationsService.error('Error', err.text) | ||
68 | ) | ||
69 | } | ||
70 | |||
71 | isAddButtonDisplayed () { | ||
72 | return this.form.value['text'] | ||
73 | } | ||
74 | |||
75 | private addCommentReply (commentCreate: VideoCommentCreate) { | ||
76 | return this.videoCommentService | ||
77 | .addCommentReply(this.video.id, this.parentComment.id, commentCreate) | ||
78 | } | ||
79 | |||
80 | private addCommentThread (commentCreate: VideoCommentCreate) { | ||
81 | return this.videoCommentService | ||
82 | .addCommentThread(this.video.id, commentCreate) | ||
83 | } | ||
84 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.html b/client/src/app/videos/+video-watch/comment/video-comment.component.html new file mode 100644 index 000000000..9608a1033 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.html | |||
@@ -0,0 +1,29 @@ | |||
1 | <div class="comment"> | ||
2 | <div class="comment-account-date"> | ||
3 | <div class="comment-account">{{ comment.by }}</div> | ||
4 | <div class="comment-date">{{ comment.createdAt | myFromNow }}</div> | ||
5 | </div> | ||
6 | <div>{{ comment.text }}</div> | ||
7 | |||
8 | <div class="comment-actions"> | ||
9 | <div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply">Reply</div> | ||
10 | </div> | ||
11 | |||
12 | <my-video-comment-add | ||
13 | *ngIf="isUserLoggedIn() && inReplyToCommentId === comment.id" [video]="video" [parentComment]="comment" | ||
14 | (commentCreated)="onCommentReplyCreated($event)" | ||
15 | ></my-video-comment-add> | ||
16 | |||
17 | <div *ngIf="commentTree" class="children"> | ||
18 | <div *ngFor="let commentChild of commentTree.children"> | ||
19 | <my-video-comment | ||
20 | [comment]="commentChild.comment" | ||
21 | [video]="video" | ||
22 | [inReplyToCommentId]="inReplyToCommentId" | ||
23 | [commentTree]="commentChild" | ||
24 | (wantedToReply)="onWantedToReply($event)" | ||
25 | (resetReply)="onResetReply()" | ||
26 | ></my-video-comment> | ||
27 | </div> | ||
28 | </div> | ||
29 | </div> | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.scss b/client/src/app/videos/+video-watch/comment/video-comment.component.scss new file mode 100644 index 000000000..7e1a32f48 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.scss | |||
@@ -0,0 +1,38 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .comment { | ||
5 | font-size: 15px; | ||
6 | margin-top: 30px; | ||
7 | |||
8 | .comment-account-date { | ||
9 | display: flex; | ||
10 | margin-bottom: 4px; | ||
11 | |||
12 | .comment-account { | ||
13 | font-weight: $font-bold; | ||
14 | } | ||
15 | |||
16 | .comment-date { | ||
17 | color: #585858; | ||
18 | margin-left: 10px; | ||
19 | } | ||
20 | } | ||
21 | |||
22 | .comment-actions { | ||
23 | margin: 10px 0; | ||
24 | |||
25 | .comment-action-reply { | ||
26 | color: #585858; | ||
27 | cursor: pointer; | ||
28 | } | ||
29 | } | ||
30 | } | ||
31 | |||
32 | .children { | ||
33 | margin-left: 20px; | ||
34 | |||
35 | .comment { | ||
36 | margin-top: 15px; | ||
37 | } | ||
38 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.component.ts b/client/src/app/videos/+video-watch/comment/video-comment.component.ts new file mode 100644 index 000000000..b8e2acd52 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment.component.ts | |||
@@ -0,0 +1,67 @@ | |||
1 | import { Component, EventEmitter, Input, Output } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | ||
4 | import { AuthService } from '../../../core/auth' | ||
5 | import { User } from '../../../shared/users' | ||
6 | import { Video } from '../../../shared/video/video.model' | ||
7 | import { VideoComment } from './video-comment.model' | ||
8 | import { VideoCommentService } from './video-comment.service' | ||
9 | |||
10 | @Component({ | ||
11 | selector: 'my-video-comment', | ||
12 | templateUrl: './video-comment.component.html', | ||
13 | styleUrls: ['./video-comment.component.scss'] | ||
14 | }) | ||
15 | export class VideoCommentComponent { | ||
16 | @Input() video: Video | ||
17 | @Input() comment: VideoComment | ||
18 | @Input() commentTree: VideoCommentThreadTree | ||
19 | @Input() inReplyToCommentId: number | ||
20 | |||
21 | @Output() wantedToReply = new EventEmitter<VideoComment>() | ||
22 | @Output() resetReply = new EventEmitter() | ||
23 | |||
24 | constructor (private authService: AuthService, | ||
25 | private notificationsService: NotificationsService, | ||
26 | private videoCommentService: VideoCommentService) { | ||
27 | } | ||
28 | |||
29 | onCommentReplyCreated (comment: VideoComment) { | ||
30 | this.videoCommentService.addCommentReply(this.video.id, this.comment.id, comment) | ||
31 | .subscribe( | ||
32 | createdComment => { | ||
33 | if (!this.commentTree) { | ||
34 | this.commentTree = { | ||
35 | comment: this.comment, | ||
36 | children: [] | ||
37 | } | ||
38 | } | ||
39 | |||
40 | this.commentTree.children.push({ | ||
41 | comment: createdComment, | ||
42 | children: [] | ||
43 | }) | ||
44 | this.resetReply.emit() | ||
45 | }, | ||
46 | |||
47 | err => this.notificationsService.error('Error', err.message) | ||
48 | ) | ||
49 | } | ||
50 | |||
51 | onWantToReply () { | ||
52 | this.wantedToReply.emit(this.comment) | ||
53 | } | ||
54 | |||
55 | isUserLoggedIn () { | ||
56 | return this.authService.isLoggedIn() | ||
57 | } | ||
58 | |||
59 | // Event from child comment | ||
60 | onWantedToReply (comment: VideoComment) { | ||
61 | this.wantedToReply.emit(comment) | ||
62 | } | ||
63 | |||
64 | onResetReply () { | ||
65 | this.resetReply.emit() | ||
66 | } | ||
67 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.model.ts b/client/src/app/videos/+video-watch/comment/video-comment.model.ts new file mode 100644 index 000000000..df7d5244c --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment.model.ts | |||
@@ -0,0 +1,38 @@ | |||
1 | import { VideoComment as VideoCommentServerModel } from '../../../../../../shared/models/videos/video-comment.model' | ||
2 | |||
3 | export class VideoComment implements VideoCommentServerModel { | ||
4 | id: number | ||
5 | url: string | ||
6 | text: string | ||
7 | threadId: number | ||
8 | inReplyToCommentId: number | ||
9 | videoId: number | ||
10 | createdAt: Date | string | ||
11 | updatedAt: Date | string | ||
12 | account: { | ||
13 | name: string | ||
14 | host: string | ||
15 | } | ||
16 | totalReplies: number | ||
17 | |||
18 | by: string | ||
19 | |||
20 | private static createByString (account: string, serverHost: string) { | ||
21 | return account + '@' + serverHost | ||
22 | } | ||
23 | |||
24 | constructor (hash: VideoCommentServerModel) { | ||
25 | this.id = hash.id | ||
26 | this.url = hash.url | ||
27 | this.text = hash.text | ||
28 | this.threadId = hash.threadId | ||
29 | this.inReplyToCommentId = hash.inReplyToCommentId | ||
30 | this.videoId = hash.videoId | ||
31 | this.createdAt = new Date(hash.createdAt.toString()) | ||
32 | this.updatedAt = new Date(hash.updatedAt.toString()) | ||
33 | this.account = hash.account | ||
34 | this.totalReplies = hash.totalReplies | ||
35 | |||
36 | this.by = VideoComment.createByString(this.account.name, this.account.host) | ||
37 | } | ||
38 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comment.service.ts b/client/src/app/videos/+video-watch/comment/video-comment.service.ts new file mode 100644 index 000000000..2fe6cc3e9 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comment.service.ts | |||
@@ -0,0 +1,93 @@ | |||
1 | import { HttpClient, HttpParams } from '@angular/common/http' | ||
2 | import { Injectable } from '@angular/core' | ||
3 | import 'rxjs/add/operator/catch' | ||
4 | import 'rxjs/add/operator/map' | ||
5 | import { Observable } from 'rxjs/Observable' | ||
6 | import { ResultList } from '../../../../../../shared/models' | ||
7 | import { | ||
8 | VideoComment as VideoCommentServerModel, VideoCommentCreate, | ||
9 | VideoCommentThreadTree | ||
10 | } from '../../../../../../shared/models/videos/video-comment.model' | ||
11 | import { environment } from '../../../../environments/environment' | ||
12 | import { RestExtractor, RestService } from '../../../shared/rest' | ||
13 | import { ComponentPagination } from '../../../shared/rest/component-pagination.model' | ||
14 | import { SortField } from '../../../shared/video/sort-field.type' | ||
15 | import { VideoComment } from './video-comment.model' | ||
16 | |||
17 | @Injectable() | ||
18 | export class VideoCommentService { | ||
19 | private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' | ||
20 | |||
21 | constructor ( | ||
22 | private authHttp: HttpClient, | ||
23 | private restExtractor: RestExtractor, | ||
24 | private restService: RestService | ||
25 | ) {} | ||
26 | |||
27 | addCommentThread (videoId: number | string, comment: VideoCommentCreate) { | ||
28 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | ||
29 | |||
30 | return this.authHttp.post(url, comment) | ||
31 | .map(data => this.extractVideoComment(data['comment'])) | ||
32 | .catch(this.restExtractor.handleError) | ||
33 | } | ||
34 | |||
35 | addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { | ||
36 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId | ||
37 | |||
38 | return this.authHttp.post(url, comment) | ||
39 | .map(data => this.extractVideoComment(data['comment'])) | ||
40 | .catch(this.restExtractor.handleError) | ||
41 | } | ||
42 | |||
43 | getVideoCommentThreads ( | ||
44 | videoId: number | string, | ||
45 | componentPagination: ComponentPagination, | ||
46 | sort: SortField | ||
47 | ): Observable<{ comments: VideoComment[], totalComments: number}> { | ||
48 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
49 | |||
50 | let params = new HttpParams() | ||
51 | params = this.restService.addRestGetParams(params, pagination, sort) | ||
52 | |||
53 | const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' | ||
54 | return this.authHttp | ||
55 | .get(url, { params }) | ||
56 | .map(this.extractVideoComments) | ||
57 | .catch((res) => this.restExtractor.handleError(res)) | ||
58 | } | ||
59 | |||
60 | getVideoThreadComments (videoId: number | string, threadId: number): Observable<VideoCommentThreadTree> { | ||
61 | const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` | ||
62 | |||
63 | return this.authHttp | ||
64 | .get(url) | ||
65 | .map(tree => this.extractVideoCommentTree(tree as VideoCommentThreadTree)) | ||
66 | .catch((res) => this.restExtractor.handleError(res)) | ||
67 | } | ||
68 | |||
69 | private extractVideoComment (videoComment: VideoCommentServerModel) { | ||
70 | return new VideoComment(videoComment) | ||
71 | } | ||
72 | |||
73 | private extractVideoComments (result: ResultList<VideoCommentServerModel>) { | ||
74 | const videoCommentsJson = result.data | ||
75 | const totalComments = result.total | ||
76 | const comments = [] | ||
77 | |||
78 | for (const videoCommentJson of videoCommentsJson) { | ||
79 | comments.push(new VideoComment(videoCommentJson)) | ||
80 | } | ||
81 | |||
82 | return { comments, totalComments } | ||
83 | } | ||
84 | |||
85 | private extractVideoCommentTree (tree: VideoCommentThreadTree) { | ||
86 | if (!tree) return tree | ||
87 | |||
88 | tree.comment = new VideoComment(tree.comment) | ||
89 | tree.children.forEach(c => this.extractVideoCommentTree(c)) | ||
90 | |||
91 | return tree | ||
92 | } | ||
93 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.html b/client/src/app/videos/+video-watch/comment/video-comments.component.html new file mode 100644 index 000000000..9d7581269 --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.html | |||
@@ -0,0 +1,31 @@ | |||
1 | <div> | ||
2 | <div class="title-page title-page-single"> | ||
3 | Comments | ||
4 | </div> | ||
5 | |||
6 | <my-video-comment-add | ||
7 | *ngIf="isUserLoggedIn()" | ||
8 | [video]="video" | ||
9 | (commentCreated)="onCommentThreadCreated($event)" | ||
10 | ></my-video-comment-add> | ||
11 | |||
12 | <div class="comment-threads"> | ||
13 | <div *ngFor="let comment of comments"> | ||
14 | <my-video-comment | ||
15 | [comment]="comment" | ||
16 | [video]="video" | ||
17 | [inReplyToCommentId]="inReplyToCommentId" | ||
18 | [commentTree]="threadComments[comment.id]" | ||
19 | (wantedToReply)="onWantedToReply($event)" | ||
20 | (resetReply)="onResetReply()" | ||
21 | ></my-video-comment> | ||
22 | |||
23 | <div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment)" class="view-replies"> | ||
24 | View all {{ comment.totalReplies }} replies | ||
25 | |||
26 | <span *ngIf="!threadLoading[comment.id]" class="glyphicon glyphicon-menu-down"></span> | ||
27 | <my-loader class="comment-thread-loading" [loading]="threadLoading[comment.id]"></my-loader> | ||
28 | </div> | ||
29 | </div> | ||
30 | </div> | ||
31 | </div> | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.scss b/client/src/app/videos/+video-watch/comment/video-comments.component.scss new file mode 100644 index 000000000..2f6e4663b --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.scss | |||
@@ -0,0 +1,14 @@ | |||
1 | @import '_variables'; | ||
2 | @import '_mixins'; | ||
3 | |||
4 | .view-replies { | ||
5 | font-weight: $font-semibold; | ||
6 | font-size: 15px; | ||
7 | cursor: pointer; | ||
8 | } | ||
9 | |||
10 | .glyphicon, .comment-thread-loading { | ||
11 | margin-left: 5px; | ||
12 | display: inline-block; | ||
13 | font-size: 13px; | ||
14 | } | ||
diff --git a/client/src/app/videos/+video-watch/comment/video-comments.component.ts b/client/src/app/videos/+video-watch/comment/video-comments.component.ts new file mode 100644 index 000000000..32e0f2fbd --- /dev/null +++ b/client/src/app/videos/+video-watch/comment/video-comments.component.ts | |||
@@ -0,0 +1,79 @@ | |||
1 | import { Component, Input, OnInit } from '@angular/core' | ||
2 | import { NotificationsService } from 'angular2-notifications' | ||
3 | import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' | ||
4 | import { AuthService } from '../../../core/auth' | ||
5 | import { ComponentPagination } from '../../../shared/rest/component-pagination.model' | ||
6 | import { User } from '../../../shared/users' | ||
7 | import { SortField } from '../../../shared/video/sort-field.type' | ||
8 | import { Video } from '../../../shared/video/video.model' | ||
9 | import { VideoComment } from './video-comment.model' | ||
10 | import { VideoCommentService } from './video-comment.service' | ||
11 | |||
12 | @Component({ | ||
13 | selector: 'my-video-comments', | ||
14 | templateUrl: './video-comments.component.html', | ||
15 | styleUrls: ['./video-comments.component.scss'] | ||
16 | }) | ||
17 | export class VideoCommentsComponent implements OnInit { | ||
18 | @Input() video: Video | ||
19 | @Input() user: User | ||
20 | |||
21 | comments: VideoComment[] = [] | ||
22 | sort: SortField = '-createdAt' | ||
23 | componentPagination: ComponentPagination = { | ||
24 | currentPage: 1, | ||
25 | itemsPerPage: 25, | ||
26 | totalItems: null | ||
27 | } | ||
28 | inReplyToCommentId: number | ||
29 | threadComments: { [ id: number ]: VideoCommentThreadTree } = {} | ||
30 | threadLoading: { [ id: number ]: boolean } = {} | ||
31 | |||
32 | constructor ( | ||
33 | private authService: AuthService, | ||
34 | private notificationsService: NotificationsService, | ||
35 | private videoCommentService: VideoCommentService | ||
36 | ) {} | ||
37 | |||
38 | ngOnInit () { | ||
39 | this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort) | ||
40 | .subscribe( | ||
41 | res => { | ||
42 | this.comments = res.comments | ||
43 | this.componentPagination.totalItems = res.totalComments | ||
44 | }, | ||
45 | |||
46 | err => this.notificationsService.error('Error', err.message) | ||
47 | ) | ||
48 | } | ||
49 | |||
50 | viewReplies (comment: VideoComment) { | ||
51 | this.threadLoading[comment.id] = true | ||
52 | |||
53 | this.videoCommentService.getVideoThreadComments(this.video.id, comment.id) | ||
54 | .subscribe( | ||
55 | res => { | ||
56 | this.threadComments[comment.id] = res | ||
57 | this.threadLoading[comment.id] = false | ||
58 | }, | ||
59 | |||
60 | err => this.notificationsService.error('Error', err.message) | ||
61 | ) | ||
62 | } | ||
63 | |||
64 | onCommentThreadCreated (comment: VideoComment) { | ||
65 | this.comments.unshift(comment) | ||
66 | } | ||
67 | |||
68 | onWantedToReply (comment: VideoComment) { | ||
69 | this.inReplyToCommentId = comment.id | ||
70 | } | ||
71 | |||
72 | onResetReply () { | ||
73 | this.inReplyToCommentId = undefined | ||
74 | } | ||
75 | |||
76 | isUserLoggedIn () { | ||
77 | return this.authService.isLoggedIn() | ||
78 | } | ||
79 | } | ||
diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html index f8f17a471..f8f17a471 100644 --- a/client/src/app/videos/+video-watch/video-download.component.html +++ b/client/src/app/videos/+video-watch/modal/video-download.component.html | |||
diff --git a/client/src/app/videos/+video-watch/video-download.component.scss b/client/src/app/videos/+video-watch/modal/video-download.component.scss index 5fca82135..6325f67a3 100644 --- a/client/src/app/videos/+video-watch/video-download.component.scss +++ b/client/src/app/videos/+video-watch/modal/video-download.component.scss | |||
@@ -1,5 +1,5 @@ | |||
1 | @import '_variables'; | 1 | @import 'variables'; |
2 | @import '_mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | .peertube-select-container { | 4 | .peertube-select-container { |
5 | @include peertube-select-container(130px); | 5 | @include peertube-select-container(130px); |
diff --git a/client/src/app/videos/+video-watch/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts index 44ece986c..1a73ea6df 100644 --- a/client/src/app/videos/+video-watch/video-download.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' | 1 | import { Component, Input, OnInit, ViewChild } from '@angular/core' |
2 | import { ModalDirective } from 'ngx-bootstrap/modal' | 2 | import { ModalDirective } from 'ngx-bootstrap/modal' |
3 | import { VideoDetails } from '../../shared/video/video-details.model' | 3 | import { VideoDetails } from '../../../shared/video/video-details.model' |
4 | 4 | ||
5 | @Component({ | 5 | @Component({ |
6 | selector: 'my-video-download', | 6 | selector: 'my-video-download', |
diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html index a9a7beb48..a9a7beb48 100644 --- a/client/src/app/videos/+video-watch/video-report.component.html +++ b/client/src/app/videos/+video-watch/modal/video-report.component.html | |||
diff --git a/client/src/app/videos/+video-watch/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss index 09d273b35..84562f15c 100644 --- a/client/src/app/videos/+video-watch/video-report.component.scss +++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss | |||
@@ -1,5 +1,5 @@ | |||
1 | @import '_variables'; | 1 | @import 'variables'; |
2 | @import '_mixins'; | 2 | @import 'mixins'; |
3 | 3 | ||
4 | textarea { | 4 | textarea { |
5 | @include peertube-textarea(100%, 60px); | 5 | @include peertube-textarea(100%, 60px); |
diff --git a/client/src/app/videos/+video-watch/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts index ece14754a..050e827e7 100644 --- a/client/src/app/videos/+video-watch/video-report.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts | |||
@@ -2,8 +2,8 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core' | |||
2 | import { FormBuilder, FormGroup } from '@angular/forms' | 2 | import { FormBuilder, FormGroup } from '@angular/forms' |
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | import { ModalDirective } from 'ngx-bootstrap/modal' | 4 | import { ModalDirective } from 'ngx-bootstrap/modal' |
5 | import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared' | 5 | import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../../shared/index' |
6 | import { VideoDetails } from '../../shared/video/video-details.model' | 6 | import { VideoDetails } from '../../../shared/video/video-details.model' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-video-report', | 9 | selector: 'my-video-report', |
diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html index 85cf10a6c..85cf10a6c 100644 --- a/client/src/app/videos/+video-watch/video-share.component.html +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html | |||
diff --git a/client/src/app/videos/+video-watch/video-share.component.scss b/client/src/app/videos/+video-watch/modal/video-share.component.scss index 184e09027..184e09027 100644 --- a/client/src/app/videos/+video-watch/video-share.component.scss +++ b/client/src/app/videos/+video-watch/modal/video-share.component.scss | |||
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts index 0664c28be..678cccfb5 100644 --- a/client/src/app/videos/+video-watch/video-share.component.ts +++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts | |||
@@ -3,7 +3,7 @@ import { Component, Input, ViewChild } from '@angular/core' | |||
3 | import { NotificationsService } from 'angular2-notifications' | 3 | import { NotificationsService } from 'angular2-notifications' |
4 | 4 | ||
5 | import { ModalDirective } from 'ngx-bootstrap/modal' | 5 | import { ModalDirective } from 'ngx-bootstrap/modal' |
6 | import { VideoDetails } from '../../shared/video/video-details.model' | 6 | import { VideoDetails } from '../../../shared/video/video-details.model' |
7 | 7 | ||
8 | @Component({ | 8 | @Component({ |
9 | selector: 'my-video-share', | 9 | selector: 'my-video-share', |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html index 860edecd2..48d1bb474 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.html +++ b/client/src/app/videos/+video-watch/video-watch.component.html | |||
@@ -54,6 +54,12 @@ | |||
54 | </a> | 54 | </a> |
55 | </li> | 55 | </li> |
56 | 56 | ||
57 | <li *ngIf="isVideoUpdatable()" role="menuitem"> | ||
58 | <a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]"> | ||
59 | <span class="icon icon-edit"></span> Update | ||
60 | </a> | ||
61 | </li> | ||
62 | |||
57 | <li *ngIf="isVideoRemovable()" role="menuitem"> | 63 | <li *ngIf="isVideoRemovable()" role="menuitem"> |
58 | <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> | 64 | <a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)"> |
59 | <span class="icon icon-blacklist"></span> Delete | 65 | <span class="icon icon-blacklist"></span> Delete |
@@ -149,6 +155,7 @@ | |||
149 | </div> | 155 | </div> |
150 | </div> | 156 | </div> |
151 | 157 | ||
158 | <my-video-comments [video]="video" [user]="user"></my-video-comments> | ||
152 | </div> | 159 | </div> |
153 | 160 | ||
154 | <div class="other-videos"> | 161 | <div class="other-videos"> |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss index b37fa3d61..c101aa04e 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.scss +++ b/client/src/app/videos/+video-watch/video-watch.component.scss | |||
@@ -126,6 +126,10 @@ | |||
126 | background-image: url('../../../assets/images/video/download-black.svg'); | 126 | background-image: url('../../../assets/images/video/download-black.svg'); |
127 | } | 127 | } |
128 | 128 | ||
129 | &.icon-edit { | ||
130 | background-image: url('../../../assets/images/global/edit-black.svg'); | ||
131 | } | ||
132 | |||
129 | &.icon-alert { | 133 | &.icon-alert { |
130 | background-image: url('../../../assets/images/video/alert.svg'); | 134 | background-image: url('../../../assets/images/video/alert.svg'); |
131 | } | 135 | } |
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 c388b138b..4afd6160c 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -14,9 +14,9 @@ import { VideoDetails } from '../../shared/video/video-details.model' | |||
14 | import { Video } from '../../shared/video/video.model' | 14 | import { Video } from '../../shared/video/video.model' |
15 | import { VideoService } from '../../shared/video/video.service' | 15 | import { VideoService } from '../../shared/video/video.service' |
16 | import { MarkdownService } from '../shared' | 16 | import { MarkdownService } from '../shared' |
17 | import { VideoDownloadComponent } from './video-download.component' | 17 | import { VideoDownloadComponent } from './modal/video-download.component' |
18 | import { VideoReportComponent } from './video-report.component' | 18 | import { VideoReportComponent } from './modal/video-report.component' |
19 | import { VideoShareComponent } from './video-share.component' | 19 | import { VideoShareComponent } from './modal/video-share.component' |
20 | 20 | ||
21 | @Component({ | 21 | @Component({ |
22 | selector: 'my-video-watch', | 22 | selector: 'my-video-watch', |
@@ -208,6 +208,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
208 | return this.authService.isLoggedIn() | 208 | return this.authService.isLoggedIn() |
209 | } | 209 | } |
210 | 210 | ||
211 | isVideoUpdatable () { | ||
212 | return this.video.isUpdatableBy(this.authService.getUser()) | ||
213 | } | ||
214 | |||
211 | isVideoBlacklistable () { | 215 | isVideoBlacklistable () { |
212 | return this.video.isBlackistableBy(this.user) | 216 | return this.video.isBlackistableBy(this.user) |
213 | } | 217 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.module.ts b/client/src/app/videos/+video-watch/video-watch.module.ts index e77883472..085a9ec5a 100644 --- a/client/src/app/videos/+video-watch/video-watch.module.ts +++ b/client/src/app/videos/+video-watch/video-watch.module.ts | |||
@@ -3,9 +3,13 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip' | |||
3 | import { ClipboardModule } from 'ngx-clipboard' | 3 | import { ClipboardModule } from 'ngx-clipboard' |
4 | import { SharedModule } from '../../shared' | 4 | import { SharedModule } from '../../shared' |
5 | import { MarkdownService } from '../shared' | 5 | import { MarkdownService } from '../shared' |
6 | import { VideoDownloadComponent } from './video-download.component' | 6 | import { VideoCommentAddComponent } from './comment/video-comment-add.component' |
7 | import { VideoReportComponent } from './video-report.component' | 7 | import { VideoCommentComponent } from './comment/video-comment.component' |
8 | import { VideoShareComponent } from './video-share.component' | 8 | import { VideoCommentService } from './comment/video-comment.service' |
9 | import { VideoCommentsComponent } from './comment/video-comments.component' | ||
10 | import { VideoDownloadComponent } from './modal/video-download.component' | ||
11 | import { VideoReportComponent } from './modal/video-report.component' | ||
12 | import { VideoShareComponent } from './modal/video-share.component' | ||
9 | 13 | ||
10 | import { VideoWatchRoutingModule } from './video-watch-routing.module' | 14 | import { VideoWatchRoutingModule } from './video-watch-routing.module' |
11 | 15 | ||
@@ -24,7 +28,10 @@ import { VideoWatchComponent } from './video-watch.component' | |||
24 | 28 | ||
25 | VideoDownloadComponent, | 29 | VideoDownloadComponent, |
26 | VideoShareComponent, | 30 | VideoShareComponent, |
27 | VideoReportComponent | 31 | VideoReportComponent, |
32 | VideoCommentsComponent, | ||
33 | VideoCommentAddComponent, | ||
34 | VideoCommentComponent | ||
28 | ], | 35 | ], |
29 | 36 | ||
30 | exports: [ | 37 | exports: [ |
@@ -32,7 +39,8 @@ import { VideoWatchComponent } from './video-watch.component' | |||
32 | ], | 39 | ], |
33 | 40 | ||
34 | providers: [ | 41 | providers: [ |
35 | MarkdownService | 42 | MarkdownService, |
43 | VideoCommentService | ||
36 | ] | 44 | ] |
37 | }) | 45 | }) |
38 | export class VideoWatchModule { } | 46 | export class VideoWatchModule { } |