From 4635f59d7c3fea4b97029f10886c62fdf38b2084 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 27 Dec 2017 16:11:53 +0100 Subject: Add video comment components --- .../comment/video-comment-add.component.html | 15 ++++ .../comment/video-comment-add.component.scss | 20 +++++ .../comment/video-comment-add.component.ts | 84 +++++++++++++++++++ .../comment/video-comment.component.html | 29 +++++++ .../comment/video-comment.component.scss | 38 +++++++++ .../comment/video-comment.component.ts | 67 ++++++++++++++++ .../+video-watch/comment/video-comment.model.ts | 38 +++++++++ .../+video-watch/comment/video-comment.service.ts | 93 ++++++++++++++++++++++ .../comment/video-comments.component.html | 31 ++++++++ .../comment/video-comments.component.scss | 14 ++++ .../comment/video-comments.component.ts | 79 ++++++++++++++++++ 11 files changed, 508 insertions(+) create mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.html create mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.scss create mode 100644 client/src/app/videos/+video-watch/comment/video-comment-add.component.ts create mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.html create mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.scss create mode 100644 client/src/app/videos/+video-watch/comment/video-comment.component.ts create mode 100644 client/src/app/videos/+video-watch/comment/video-comment.model.ts create mode 100644 client/src/app/videos/+video-watch/comment/video-comment.service.ts create mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.html create mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.scss create mode 100644 client/src/app/videos/+video-watch/comment/video-comments.component.ts (limited to 'client/src/app/videos/+video-watch/comment') 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 @@ +
+
+ +
+ {{ formErrors.text }} +
+
+ +
+ +
+
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 @@ +@import '_variables'; +@import '_mixins'; + +.form-group { + margin-bottom: 10px; +} + +textarea { + @include peertube-textarea(100%, 150px); +} + +.submit-comment { + display: flex; + justify-content: end; + + button { + @include peertube-button; + @include orange-button + } +} 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 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core' +import { FormBuilder, FormGroup } from '@angular/forms' +import { NotificationsService } from 'angular2-notifications' +import { Observable } from 'rxjs/Observable' +import { VideoCommentCreate } from '../../../../../../shared/models/videos/video-comment.model' +import { FormReactive } from '../../../shared' +import { VIDEO_COMMENT_TEXT } from '../../../shared/forms/form-validators/video-comment' +import { Video } from '../../../shared/video/video.model' +import { VideoComment } from './video-comment.model' +import { VideoCommentService } from './video-comment.service' + +@Component({ + selector: 'my-video-comment-add', + templateUrl: './video-comment-add.component.html', + styleUrls: ['./video-comment-add.component.scss'] +}) +export class VideoCommentAddComponent extends FormReactive implements OnInit { + @Input() video: Video + @Input() parentComment: VideoComment + + @Output() commentCreated = new EventEmitter() + + form: FormGroup + formErrors = { + 'text': '' + } + validationMessages = { + 'text': VIDEO_COMMENT_TEXT.MESSAGES + } + + constructor ( + private formBuilder: FormBuilder, + private notificationsService: NotificationsService, + private videoCommentService: VideoCommentService + ) { + super() + } + + buildForm () { + this.form = this.formBuilder.group({ + text: [ '', VIDEO_COMMENT_TEXT.VALIDATORS ] + }) + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)) + } + + ngOnInit () { + this.buildForm() + } + + formValidated () { + const commentCreate: VideoCommentCreate = this.form.value + let obs: Observable + + if (this.parentComment) { + obs = this.addCommentReply(commentCreate) + } else { + obs = this.addCommentThread(commentCreate) + } + + obs.subscribe( + comment => { + this.commentCreated.emit(comment) + this.form.reset() + }, + + err => this.notificationsService.error('Error', err.text) + ) +} + + isAddButtonDisplayed () { + return this.form.value['text'] + } + + private addCommentReply (commentCreate: VideoCommentCreate) { + return this.videoCommentService + .addCommentReply(this.video.id, this.parentComment.id, commentCreate) + } + + private addCommentThread (commentCreate: VideoCommentCreate) { + return this.videoCommentService + .addCommentThread(this.video.id, commentCreate) + } +} 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 @@ +
+ +
{{ comment.text }}
+ +
+
Reply
+
+ + + +
+
+ +
+
+
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 @@ +@import '_variables'; +@import '_mixins'; + +.comment { + font-size: 15px; + margin-top: 30px; + + .comment-account-date { + display: flex; + margin-bottom: 4px; + + .comment-account { + font-weight: $font-bold; + } + + .comment-date { + color: #585858; + margin-left: 10px; + } + } + + .comment-actions { + margin: 10px 0; + + .comment-action-reply { + color: #585858; + cursor: pointer; + } + } +} + +.children { + margin-left: 20px; + + .comment { + margin-top: 15px; + } +} 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 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core' +import { NotificationsService } from 'angular2-notifications' +import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' +import { AuthService } from '../../../core/auth' +import { User } from '../../../shared/users' +import { Video } from '../../../shared/video/video.model' +import { VideoComment } from './video-comment.model' +import { VideoCommentService } from './video-comment.service' + +@Component({ + selector: 'my-video-comment', + templateUrl: './video-comment.component.html', + styleUrls: ['./video-comment.component.scss'] +}) +export class VideoCommentComponent { + @Input() video: Video + @Input() comment: VideoComment + @Input() commentTree: VideoCommentThreadTree + @Input() inReplyToCommentId: number + + @Output() wantedToReply = new EventEmitter() + @Output() resetReply = new EventEmitter() + + constructor (private authService: AuthService, + private notificationsService: NotificationsService, + private videoCommentService: VideoCommentService) { + } + + onCommentReplyCreated (comment: VideoComment) { + this.videoCommentService.addCommentReply(this.video.id, this.comment.id, comment) + .subscribe( + createdComment => { + if (!this.commentTree) { + this.commentTree = { + comment: this.comment, + children: [] + } + } + + this.commentTree.children.push({ + comment: createdComment, + children: [] + }) + this.resetReply.emit() + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + onWantToReply () { + this.wantedToReply.emit(this.comment) + } + + isUserLoggedIn () { + return this.authService.isLoggedIn() + } + + // Event from child comment + onWantedToReply (comment: VideoComment) { + this.wantedToReply.emit(comment) + } + + onResetReply () { + this.resetReply.emit() + } +} 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 @@ +import { VideoComment as VideoCommentServerModel } from '../../../../../../shared/models/videos/video-comment.model' + +export class VideoComment implements VideoCommentServerModel { + id: number + url: string + text: string + threadId: number + inReplyToCommentId: number + videoId: number + createdAt: Date | string + updatedAt: Date | string + account: { + name: string + host: string + } + totalReplies: number + + by: string + + private static createByString (account: string, serverHost: string) { + return account + '@' + serverHost + } + + constructor (hash: VideoCommentServerModel) { + this.id = hash.id + this.url = hash.url + this.text = hash.text + this.threadId = hash.threadId + this.inReplyToCommentId = hash.inReplyToCommentId + this.videoId = hash.videoId + this.createdAt = new Date(hash.createdAt.toString()) + this.updatedAt = new Date(hash.updatedAt.toString()) + this.account = hash.account + this.totalReplies = hash.totalReplies + + this.by = VideoComment.createByString(this.account.name, this.account.host) + } +} 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 @@ +import { HttpClient, HttpParams } from '@angular/common/http' +import { Injectable } from '@angular/core' +import 'rxjs/add/operator/catch' +import 'rxjs/add/operator/map' +import { Observable } from 'rxjs/Observable' +import { ResultList } from '../../../../../../shared/models' +import { + VideoComment as VideoCommentServerModel, VideoCommentCreate, + VideoCommentThreadTree +} from '../../../../../../shared/models/videos/video-comment.model' +import { environment } from '../../../../environments/environment' +import { RestExtractor, RestService } from '../../../shared/rest' +import { ComponentPagination } from '../../../shared/rest/component-pagination.model' +import { SortField } from '../../../shared/video/sort-field.type' +import { VideoComment } from './video-comment.model' + +@Injectable() +export class VideoCommentService { + private static BASE_VIDEO_URL = environment.apiUrl + '/api/v1/videos/' + + constructor ( + private authHttp: HttpClient, + private restExtractor: RestExtractor, + private restService: RestService + ) {} + + addCommentThread (videoId: number | string, comment: VideoCommentCreate) { + const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' + + return this.authHttp.post(url, comment) + .map(data => this.extractVideoComment(data['comment'])) + .catch(this.restExtractor.handleError) + } + + addCommentReply (videoId: number | string, inReplyToCommentId: number, comment: VideoCommentCreate) { + const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comments/' + inReplyToCommentId + + return this.authHttp.post(url, comment) + .map(data => this.extractVideoComment(data['comment'])) + .catch(this.restExtractor.handleError) + } + + getVideoCommentThreads ( + videoId: number | string, + componentPagination: ComponentPagination, + sort: SortField + ): Observable<{ comments: VideoComment[], totalComments: number}> { + const pagination = this.restService.componentPaginationToRestPagination(componentPagination) + + let params = new HttpParams() + params = this.restService.addRestGetParams(params, pagination, sort) + + const url = VideoCommentService.BASE_VIDEO_URL + videoId + '/comment-threads' + return this.authHttp + .get(url, { params }) + .map(this.extractVideoComments) + .catch((res) => this.restExtractor.handleError(res)) + } + + getVideoThreadComments (videoId: number | string, threadId: number): Observable { + const url = `${VideoCommentService.BASE_VIDEO_URL + videoId}/comment-threads/${threadId}` + + return this.authHttp + .get(url) + .map(tree => this.extractVideoCommentTree(tree as VideoCommentThreadTree)) + .catch((res) => this.restExtractor.handleError(res)) + } + + private extractVideoComment (videoComment: VideoCommentServerModel) { + return new VideoComment(videoComment) + } + + private extractVideoComments (result: ResultList) { + const videoCommentsJson = result.data + const totalComments = result.total + const comments = [] + + for (const videoCommentJson of videoCommentsJson) { + comments.push(new VideoComment(videoCommentJson)) + } + + return { comments, totalComments } + } + + private extractVideoCommentTree (tree: VideoCommentThreadTree) { + if (!tree) return tree + + tree.comment = new VideoComment(tree.comment) + tree.children.forEach(c => this.extractVideoCommentTree(c)) + + return tree + } +} 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 @@ +
+
+ Comments +
+ + + +
+
+ + +
+ View all {{ comment.totalReplies }} replies + + + +
+
+
+
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 @@ +@import '_variables'; +@import '_mixins'; + +.view-replies { + font-weight: $font-semibold; + font-size: 15px; + cursor: pointer; +} + +.glyphicon, .comment-thread-loading { + margin-left: 5px; + display: inline-block; + font-size: 13px; +} 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 @@ +import { Component, Input, OnInit } from '@angular/core' +import { NotificationsService } from 'angular2-notifications' +import { VideoCommentThreadTree } from '../../../../../../shared/models/videos/video-comment.model' +import { AuthService } from '../../../core/auth' +import { ComponentPagination } from '../../../shared/rest/component-pagination.model' +import { User } from '../../../shared/users' +import { SortField } from '../../../shared/video/sort-field.type' +import { Video } from '../../../shared/video/video.model' +import { VideoComment } from './video-comment.model' +import { VideoCommentService } from './video-comment.service' + +@Component({ + selector: 'my-video-comments', + templateUrl: './video-comments.component.html', + styleUrls: ['./video-comments.component.scss'] +}) +export class VideoCommentsComponent implements OnInit { + @Input() video: Video + @Input() user: User + + comments: VideoComment[] = [] + sort: SortField = '-createdAt' + componentPagination: ComponentPagination = { + currentPage: 1, + itemsPerPage: 25, + totalItems: null + } + inReplyToCommentId: number + threadComments: { [ id: number ]: VideoCommentThreadTree } = {} + threadLoading: { [ id: number ]: boolean } = {} + + constructor ( + private authService: AuthService, + private notificationsService: NotificationsService, + private videoCommentService: VideoCommentService + ) {} + + ngOnInit () { + this.videoCommentService.getVideoCommentThreads(this.video.id, this.componentPagination, this.sort) + .subscribe( + res => { + this.comments = res.comments + this.componentPagination.totalItems = res.totalComments + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + viewReplies (comment: VideoComment) { + this.threadLoading[comment.id] = true + + this.videoCommentService.getVideoThreadComments(this.video.id, comment.id) + .subscribe( + res => { + this.threadComments[comment.id] = res + this.threadLoading[comment.id] = false + }, + + err => this.notificationsService.error('Error', err.message) + ) + } + + onCommentThreadCreated (comment: VideoComment) { + this.comments.unshift(comment) + } + + onWantedToReply (comment: VideoComment) { + this.inReplyToCommentId = comment.id + } + + onResetReply () { + this.inReplyToCommentId = undefined + } + + isUserLoggedIn () { + return this.authService.isLoggedIn() + } +} -- cgit v1.2.3