aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src/app/videos/+video-watch/comment
diff options
context:
space:
mode:
Diffstat (limited to 'client/src/app/videos/+video-watch/comment')
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.html15
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.scss20
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment-add.component.ts84
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.html29
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.scss38
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.component.ts67
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.model.ts38
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comment.service.ts93
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.html31
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.scss14
-rw-r--r--client/src/app/videos/+video-watch/comment/video-comments.component.ts79
11 files changed, 508 insertions, 0 deletions
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
8textarea {
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 @@
1import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'
2import { FormBuilder, FormGroup } from '@angular/forms'
3import { NotificationsService } from 'angular2-notifications'
4import { Observable } from 'rxjs/Observable'
5import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model'
6import { FormReactive } from '../../../shared'
7import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment'
8import { Video } from '../../../shared/video/video.model'
9import { VideoComment } from './video-comment.model'
10import { 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})
17export 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 @@
1import { Component, EventEmitter, Input, Output } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
4import { AuthService } from '../../../core/auth'
5import { User } from '../../../shared/users'
6import { Video } from '../../../shared/video/video.model'
7import { VideoComment } from './video-comment.model'
8import { 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})
15export 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 @@
1import { VideoComment as VideoCommentServerModel } from '../../../../../../shared/models/videos/video-comment.model'
2
3export 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 @@
1import { HttpClient, HttpParams } from '@angular/common/http'
2import { Injectable } from '@angular/core'
3import 'rxjs/add/operator/catch'
4import 'rxjs/add/operator/map'
5import { Observable } from 'rxjs/Observable'
6import { ResultList } from '../../../../../../shared/models'
7import {
8 VideoComment as VideoCommentServerModel, VideoCommentCreate,
9 VideoCommentThreadTree
10} from '../../../../../../shared/models/videos/video-comment.model'
11import { environment } from '../../../../environments/environment'
12import { RestExtractor, RestService } from '../../../shared/rest'
13import { ComponentPagination } from '../../../shared/rest/component-pagination.model'
14import { SortField } from '../../../shared/video/sort-field.type'
15import { VideoComment } from './video-comment.model'
16
17@Injectable()
18export 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 @@
1import { Component, Input, OnInit } from '@angular/core'
2import { NotificationsService } from 'angular2-notifications'
3import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model'
4import { AuthService } from '../../../core/auth'
5import { ComponentPagination } from '../../../shared/rest/component-pagination.model'
6import { User } from '../../../shared/users'
7import { SortField } from '../../../shared/video/sort-field.type'
8import { Video } from '../../../shared/video/video.model'
9import { VideoComment } from './video-comment.model'
10import { 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})
17export 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}