From: Chocobozzz Date: Wed, 27 Dec 2017 15:11:53 +0000 (+0100) Subject: Add video comment components X-Git-Tag: v0.0.1-alpha~84 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=4635f59d7c3fea4b97029f10886c62fdf38b2084;p=oweals%2Fpeertube.git Add video comment components --- 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 @@ +import { Validators } from '@angular/forms' + +export const VIDEO_COMMENT_TEXT = { + VALIDATORS: [ Validators.required, Validators.minLength(2), Validators.maxLength(3000) ], + MESSAGES: { + 'required': 'Comment is required.', + 'minlength': 'Comment must be at least 2 characters long.', + 'maxlength': 'Comment cannot be more than 3000 characters long.' + } +} 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 @@ top: -2px; &.icon-edit { - background-image: url('../../../assets/images/global/edit.svg'); + background-image: url('../../../assets/images/global/edit-grey.svg'); } &.icon-delete-grey { diff --git a/client/src/app/shared/rest/component-pagination.model.ts b/client/src/app/shared/rest/component-pagination.model.ts new file mode 100644 index 000000000..0b8ecc318 --- /dev/null +++ b/client/src/app/shared/rest/component-pagination.model.ts @@ -0,0 +1,5 @@ +export interface ComponentPagination { + currentPage: number + itemsPerPage: number + 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 @@ import { Injectable } from '@angular/core' import { HttpParams } from '@angular/common/http' import { SortMeta } from 'primeng/components/common/sortmeta' +import { ComponentPagination } from './component-pagination.model' import { RestPagination } from './rest-pagination' @@ -31,4 +32,10 @@ export class RestService { return newParams } + componentPaginationToRestPagination (componentPagination: ComponentPagination): RestPagination { + const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage + const count: number = componentPagination.itemsPerPage + + return { start, count } + } } 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' import { NotificationsService } from 'angular2-notifications' import { Observable } from 'rxjs/Observable' import { AuthService } from '../../core/auth' +import { ComponentPagination } from '../rest/component-pagination.model' import { SortField } from './sort-field.type' -import { VideoPagination } from './video-pagination.model' import { Video } from './video.model' export abstract class AbstractVideoList implements OnInit { - pagination: VideoPagination = { + pagination: ComponentPagination = { currentPage: 1, itemsPerPage: 25, 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 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - font-weight: bold; transition: color 0.2s; font-size: 16px; font-weight: $font-semibold; diff --git a/client/src/app/shared/video/video-pagination.model.ts b/client/src/app/shared/video/video-pagination.model.ts deleted file mode 100644 index e9db61596..000000000 --- a/client/src/app/shared/video/video-pagination.model.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface VideoPagination { - currentPage: number - itemsPerPage: number - totalItems?: number -} 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 import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type' import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model' import { environment } from '../../../environments/environment' +import { ComponentPagination } from '../rest/component-pagination.model' import { RestExtractor } from '../rest/rest-extractor.service' import { RestService } from '../rest/rest.service' import { UserService } from '../users/user.service' import { SortField } from './sort-field.type' import { VideoDetails } from './video-details.model' import { VideoEdit } from './video-edit.model' -import { VideoPagination } from './video-pagination.model' import { Video } from './video.model' @Injectable() @@ -71,8 +71,8 @@ export class VideoService { .catch(this.restExtractor.handleError) } - getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { - const pagination = this.videoPaginationToRestPagination(videoPagination) + getMyVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) @@ -82,8 +82,8 @@ export class VideoService { .catch((res) => this.restExtractor.handleError(res)) } - getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { - const pagination = this.videoPaginationToRestPagination(videoPagination) + getVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) @@ -94,10 +94,14 @@ export class VideoService { .catch((res) => this.restExtractor.handleError(res)) } - searchVideos (search: string, videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> { + searchVideos ( + search: string, + videoPagination: ComponentPagination, + sort: SortField + ): Observable<{ videos: Video[], totalVideos: number}> { const url = VideoService.BASE_VIDEO_URL + 'search' - const pagination = this.videoPaginationToRestPagination(videoPagination) + const pagination = this.restService.componentPaginationToRestPagination(videoPagination) let params = new HttpParams() params = this.restService.addRestGetParams(params, pagination, sort) @@ -139,13 +143,6 @@ export class VideoService { .catch(res => this.restExtractor.handleError(res)) } - private videoPaginationToRestPagination (videoPagination: VideoPagination) { - const start: number = (videoPagination.currentPage - 1) * videoPagination.itemsPerPage - const count: number = videoPagination.itemsPerPage - - return { start, count } - } - private setVideoRate (id: number, rateType: VideoRateType) { const url = VideoService.BASE_VIDEO_URL + id + '/rate' 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 @@ +
+
+ +
+ {{ 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() + } +} diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.html b/client/src/app/videos/+video-watch/modal/video-download.component.html new file mode 100644 index 000000000..f8f17a471 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-download.component.html @@ -0,0 +1,42 @@ + diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.scss b/client/src/app/videos/+video-watch/modal/video-download.component.scss new file mode 100644 index 000000000..6325f67a3 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-download.component.scss @@ -0,0 +1,17 @@ +@import 'variables'; +@import 'mixins'; + +.peertube-select-container { + @include peertube-select-container(130px); +} + +.download-type { + margin-top: 30px; + + .peertube-radio-container { + @include peertube-radio-container; + + display: inline-block; + margin-right: 30px; + } +} diff --git a/client/src/app/videos/+video-watch/modal/video-download.component.ts b/client/src/app/videos/+video-watch/modal/video-download.component.ts new file mode 100644 index 000000000..1a73ea6df --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-download.component.ts @@ -0,0 +1,44 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core' +import { ModalDirective } from 'ngx-bootstrap/modal' +import { VideoDetails } from '../../../shared/video/video-details.model' + +@Component({ + selector: 'my-video-download', + templateUrl: './video-download.component.html', + styleUrls: [ './video-download.component.scss' ] +}) +export class VideoDownloadComponent implements OnInit { + @Input() video: VideoDetails = null + + @ViewChild('modal') modal: ModalDirective + + downloadType: 'direct' | 'torrent' = 'torrent' + resolution = -1 + + constructor () { + // empty + } + + ngOnInit () { + this.resolution = this.video.files[0].resolution + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + download () { + const file = this.video.files.find(f => f.resolution === this.resolution) + if (!file) { + console.error('Could not find file with resolution %d.', this.resolution) + return + } + + const link = this.downloadType === 'direct' ? file.fileUrl : file.torrentUrl + window.open(link) + } +} diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.html b/client/src/app/videos/+video-watch/modal/video-report.component.html new file mode 100644 index 000000000..a9a7beb48 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-report.component.html @@ -0,0 +1,36 @@ + diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.scss b/client/src/app/videos/+video-watch/modal/video-report.component.scss new file mode 100644 index 000000000..84562f15c --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-report.component.scss @@ -0,0 +1,6 @@ +@import 'variables'; +@import 'mixins'; + +textarea { + @include peertube-textarea(100%, 60px); +} diff --git a/client/src/app/videos/+video-watch/modal/video-report.component.ts b/client/src/app/videos/+video-watch/modal/video-report.component.ts new file mode 100644 index 000000000..050e827e7 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-report.component.ts @@ -0,0 +1,68 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core' +import { FormBuilder, FormGroup } from '@angular/forms' +import { NotificationsService } from 'angular2-notifications' +import { ModalDirective } from 'ngx-bootstrap/modal' +import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../../shared/index' +import { VideoDetails } from '../../../shared/video/video-details.model' + +@Component({ + selector: 'my-video-report', + templateUrl: './video-report.component.html', + styleUrls: [ './video-report.component.scss' ] +}) +export class VideoReportComponent extends FormReactive implements OnInit { + @Input() video: VideoDetails = null + + @ViewChild('modal') modal: ModalDirective + + error: string = null + form: FormGroup + formErrors = { + reason: '' + } + validationMessages = { + reason: VIDEO_ABUSE_REASON.MESSAGES + } + + constructor ( + private formBuilder: FormBuilder, + private videoAbuseService: VideoAbuseService, + private notificationsService: NotificationsService + ) { + super() + } + + ngOnInit () { + this.buildForm() + } + + buildForm () { + this.form = this.formBuilder.group({ + reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ] + }) + + this.form.valueChanges.subscribe(data => this.onValueChanged(data)) + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + report () { + const reason = this.form.value['reason'] + + this.videoAbuseService.reportVideo(this.video.id, reason) + .subscribe( + () => { + this.notificationsService.success('Success', 'Video reported.') + this.hide() + }, + + err => this.notificationsService.error('Error', err.message) + ) + } +} diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.html b/client/src/app/videos/+video-watch/modal/video-share.component.html new file mode 100644 index 000000000..85cf10a6c --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-share.component.html @@ -0,0 +1,47 @@ + diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.scss b/client/src/app/videos/+video-watch/modal/video-share.component.scss new file mode 100644 index 000000000..184e09027 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-share.component.scss @@ -0,0 +1,3 @@ +.action-button-cancel { + margin-right: 0 !important; +} diff --git a/client/src/app/videos/+video-watch/modal/video-share.component.ts b/client/src/app/videos/+video-watch/modal/video-share.component.ts new file mode 100644 index 000000000..678cccfb5 --- /dev/null +++ b/client/src/app/videos/+video-watch/modal/video-share.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, ViewChild } from '@angular/core' + +import { NotificationsService } from 'angular2-notifications' + +import { ModalDirective } from 'ngx-bootstrap/modal' +import { VideoDetails } from '../../../shared/video/video-details.model' + +@Component({ + selector: 'my-video-share', + templateUrl: './video-share.component.html', + styleUrls: [ './video-share.component.scss' ] +}) +export class VideoShareComponent { + @Input() video: VideoDetails = null + + @ViewChild('modal') modal: ModalDirective + + constructor (private notificationsService: NotificationsService) { + // empty + } + + show () { + this.modal.show() + } + + hide () { + this.modal.hide() + } + + getVideoIframeCode () { + return '' + } + + getVideoUrl () { + return window.location.href + } + + notSecure () { + return window.location.protocol === 'http:' + } + + activateCopiedMessage () { + this.notificationsService.success('Success', 'Copied') + } +} diff --git a/client/src/app/videos/+video-watch/video-download.component.html b/client/src/app/videos/+video-watch/video-download.component.html deleted file mode 100644 index f8f17a471..000000000 --- a/client/src/app/videos/+video-watch/video-download.component.html +++ /dev/null @@ -1,42 +0,0 @@ - diff --git a/client/src/app/videos/+video-watch/video-download.component.scss b/client/src/app/videos/+video-watch/video-download.component.scss deleted file mode 100644 index 5fca82135..000000000 --- a/client/src/app/videos/+video-watch/video-download.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -.peertube-select-container { - @include peertube-select-container(130px); -} - -.download-type { - margin-top: 30px; - - .peertube-radio-container { - @include peertube-radio-container; - - display: inline-block; - margin-right: 30px; - } -} diff --git a/client/src/app/videos/+video-watch/video-download.component.ts b/client/src/app/videos/+video-watch/video-download.component.ts deleted file mode 100644 index 44ece986c..000000000 --- a/client/src/app/videos/+video-watch/video-download.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core' -import { ModalDirective } from 'ngx-bootstrap/modal' -import { VideoDetails } from '../../shared/video/video-details.model' - -@Component({ - selector: 'my-video-download', - templateUrl: './video-download.component.html', - styleUrls: [ './video-download.component.scss' ] -}) -export class VideoDownloadComponent implements OnInit { - @Input() video: VideoDetails = null - - @ViewChild('modal') modal: ModalDirective - - downloadType: 'direct' | 'torrent' = 'torrent' - resolution = -1 - - constructor () { - // empty - } - - ngOnInit () { - this.resolution = this.video.files[0].resolution - } - - show () { - this.modal.show() - } - - hide () { - this.modal.hide() - } - - download () { - const file = this.video.files.find(f => f.resolution === this.resolution) - if (!file) { - console.error('Could not find file with resolution %d.', this.resolution) - return - } - - const link = this.downloadType === 'direct' ? file.fileUrl : file.torrentUrl - window.open(link) - } -} diff --git a/client/src/app/videos/+video-watch/video-report.component.html b/client/src/app/videos/+video-watch/video-report.component.html deleted file mode 100644 index a9a7beb48..000000000 --- a/client/src/app/videos/+video-watch/video-report.component.html +++ /dev/null @@ -1,36 +0,0 @@ - diff --git a/client/src/app/videos/+video-watch/video-report.component.scss b/client/src/app/videos/+video-watch/video-report.component.scss deleted file mode 100644 index 09d273b35..000000000 --- a/client/src/app/videos/+video-watch/video-report.component.scss +++ /dev/null @@ -1,6 +0,0 @@ -@import '_variables'; -@import '_mixins'; - -textarea { - @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/video-report.component.ts deleted file mode 100644 index ece14754a..000000000 --- a/client/src/app/videos/+video-watch/video-report.component.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { Component, Input, OnInit, ViewChild } from '@angular/core' -import { FormBuilder, FormGroup } from '@angular/forms' -import { NotificationsService } from 'angular2-notifications' -import { ModalDirective } from 'ngx-bootstrap/modal' -import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared' -import { VideoDetails } from '../../shared/video/video-details.model' - -@Component({ - selector: 'my-video-report', - templateUrl: './video-report.component.html', - styleUrls: [ './video-report.component.scss' ] -}) -export class VideoReportComponent extends FormReactive implements OnInit { - @Input() video: VideoDetails = null - - @ViewChild('modal') modal: ModalDirective - - error: string = null - form: FormGroup - formErrors = { - reason: '' - } - validationMessages = { - reason: VIDEO_ABUSE_REASON.MESSAGES - } - - constructor ( - private formBuilder: FormBuilder, - private videoAbuseService: VideoAbuseService, - private notificationsService: NotificationsService - ) { - super() - } - - ngOnInit () { - this.buildForm() - } - - buildForm () { - this.form = this.formBuilder.group({ - reason: [ '', VIDEO_ABUSE_REASON.VALIDATORS ] - }) - - this.form.valueChanges.subscribe(data => this.onValueChanged(data)) - } - - show () { - this.modal.show() - } - - hide () { - this.modal.hide() - } - - report () { - const reason = this.form.value['reason'] - - this.videoAbuseService.reportVideo(this.video.id, reason) - .subscribe( - () => { - this.notificationsService.success('Success', 'Video reported.') - this.hide() - }, - - err => this.notificationsService.error('Error', err.message) - ) - } -} diff --git a/client/src/app/videos/+video-watch/video-share.component.html b/client/src/app/videos/+video-watch/video-share.component.html deleted file mode 100644 index 85cf10a6c..000000000 --- a/client/src/app/videos/+video-watch/video-share.component.html +++ /dev/null @@ -1,47 +0,0 @@ - diff --git a/client/src/app/videos/+video-watch/video-share.component.scss b/client/src/app/videos/+video-watch/video-share.component.scss deleted file mode 100644 index 184e09027..000000000 --- a/client/src/app/videos/+video-watch/video-share.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -.action-button-cancel { - margin-right: 0 !important; -} diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts deleted file mode 100644 index 0664c28be..000000000 --- a/client/src/app/videos/+video-watch/video-share.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core' - -import { NotificationsService } from 'angular2-notifications' - -import { ModalDirective } from 'ngx-bootstrap/modal' -import { VideoDetails } from '../../shared/video/video-details.model' - -@Component({ - selector: 'my-video-share', - templateUrl: './video-share.component.html', - styleUrls: [ './video-share.component.scss' ] -}) -export class VideoShareComponent { - @Input() video: VideoDetails = null - - @ViewChild('modal') modal: ModalDirective - - constructor (private notificationsService: NotificationsService) { - // empty - } - - show () { - this.modal.show() - } - - hide () { - this.modal.hide() - } - - getVideoIframeCode () { - return '' - } - - getVideoUrl () { - return window.location.href - } - - notSecure () { - return window.location.protocol === 'http:' - } - - activateCopiedMessage () { - this.notificationsService.success('Success', 'Copied') - } -} 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 @@ +
  • + + Update + +
  • +
  • Delete @@ -149,6 +155,7 @@ +
    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 @@ background-image: url('../../../assets/images/video/download-black.svg'); } + &.icon-edit { + background-image: url('../../../assets/images/global/edit-black.svg'); + } + &.icon-alert { background-image: url('../../../assets/images/video/alert.svg'); } 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' import { Video } from '../../shared/video/video.model' import { VideoService } from '../../shared/video/video.service' import { MarkdownService } from '../shared' -import { VideoDownloadComponent } from './video-download.component' -import { VideoReportComponent } from './video-report.component' -import { VideoShareComponent } from './video-share.component' +import { VideoDownloadComponent } from './modal/video-download.component' +import { VideoReportComponent } from './modal/video-report.component' +import { VideoShareComponent } from './modal/video-share.component' @Component({ selector: 'my-video-watch', @@ -208,6 +208,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy { return this.authService.isLoggedIn() } + isVideoUpdatable () { + return this.video.isUpdatableBy(this.authService.getUser()) + } + isVideoBlacklistable () { return this.video.isBlackistableBy(this.user) } 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' import { ClipboardModule } from 'ngx-clipboard' import { SharedModule } from '../../shared' import { MarkdownService } from '../shared' -import { VideoDownloadComponent } from './video-download.component' -import { VideoReportComponent } from './video-report.component' -import { VideoShareComponent } from './video-share.component' +import { VideoCommentAddComponent } from './comment/video-comment-add.component' +import { VideoCommentComponent } from './comment/video-comment.component' +import { VideoCommentService } from './comment/video-comment.service' +import { VideoCommentsComponent } from './comment/video-comments.component' +import { VideoDownloadComponent } from './modal/video-download.component' +import { VideoReportComponent } from './modal/video-report.component' +import { VideoShareComponent } from './modal/video-share.component' import { VideoWatchRoutingModule } from './video-watch-routing.module' @@ -24,7 +28,10 @@ import { VideoWatchComponent } from './video-watch.component' VideoDownloadComponent, VideoShareComponent, - VideoReportComponent + VideoReportComponent, + VideoCommentsComponent, + VideoCommentAddComponent, + VideoCommentComponent ], exports: [ @@ -32,7 +39,8 @@ import { VideoWatchComponent } from './video-watch.component' ], providers: [ - MarkdownService + MarkdownService, + VideoCommentService ] }) export class VideoWatchModule { } diff --git a/client/src/assets/images/global/edit-black.svg b/client/src/assets/images/global/edit-black.svg new file mode 100644 index 000000000..0176b0f37 --- /dev/null +++ b/client/src/assets/images/global/edit-black.svg @@ -0,0 +1,15 @@ + + + + edit + Created with Sketch. + + + + + + + + + + diff --git a/client/src/assets/images/global/edit-grey.svg b/client/src/assets/images/global/edit-grey.svg new file mode 100644 index 000000000..23ece68f1 --- /dev/null +++ b/client/src/assets/images/global/edit-grey.svg @@ -0,0 +1,15 @@ + + + + edit + Created with Sketch. + + + + + + + + + + diff --git a/client/src/assets/images/global/edit.svg b/client/src/assets/images/global/edit.svg deleted file mode 100644 index 23ece68f1..000000000 --- a/client/src/assets/images/global/edit.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - edit - Created with Sketch. - - - - - - - - - - diff --git a/client/src/sass/application.scss b/client/src/sass/application.scss index 0539ec486..253bb1b3c 100644 --- a/client/src/sass/application.scss +++ b/client/src/sass/application.scss @@ -118,10 +118,7 @@ label { // Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d .glyphicon-refresh-animate { - -animation: spin .7s infinite linear; - -ms-animation: spin .7s infinite linear; - -webkit-animation: spinw .7s infinite linear; - -moz-animation: spinm .7s infinite linear; + animation: spin .7s infinite linear; } @keyframes spin { @@ -129,16 +126,6 @@ label { to { transform: scale(1) rotate(360deg);} } -@-webkit-keyframes spinw { - from { -webkit-transform: rotate(0deg);} - to { -webkit-transform: rotate(360deg);} -} - -@-moz-keyframes spinm { - from { -moz-transform: rotate(0deg);} - to { -moz-transform: rotate(360deg);} -} - // ngprime data table customizations p-datatable { font-size: 15px !important; diff --git a/server.ts b/server.ts index f64c4ac53..05fc39acb 100644 --- a/server.ts +++ b/server.ts @@ -151,7 +151,7 @@ app.use(function (req, res, next) { }) app.use(function (err, req, res, next) { - logger.error(err) + logger.error(err, err) res.sendStatus(err.status || 500) }) diff --git a/server/controllers/api/videos/comment.ts b/server/controllers/api/videos/comment.ts index e9dbb6d1b..276948098 100644 --- a/server/controllers/api/videos/comment.ts +++ b/server/controllers/api/videos/comment.ts @@ -66,9 +66,7 @@ async function addVideoCommentThreadRetryWrapper (req: express.Request, res: exp const comment = await retryTransactionWrapper(addVideoCommentThread, options) res.json({ - comment: { - id: comment.id - } + comment: comment.toFormattedJSON() }).end() } @@ -80,7 +78,7 @@ function addVideoCommentThread (req: express.Request, res: express.Response) { text: videoCommentInfo.text, inReplyToComment: null, video: res.locals.video, - accountId: res.locals.oauth.token.User.Account.id + account: res.locals.oauth.token.User.Account }, t) }) } @@ -94,9 +92,7 @@ async function addVideoCommentReplyRetryWrapper (req: express.Request, res: expr const comment = await retryTransactionWrapper(addVideoCommentReply, options) res.json({ - comment: { - id: comment.id - } + comment: comment.toFormattedJSON() }).end() } @@ -108,7 +104,7 @@ function addVideoCommentReply (req: express.Request, res: express.Response, next text: videoCommentInfo.text, inReplyToComment: res.locals.videoComment, video: res.locals.video, - accountId: res.locals.oauth.token.User.Account.id + account: res.locals.oauth.token.User.Account }, t) }) } diff --git a/server/lib/video-comment.ts b/server/lib/video-comment.ts index ef6a8f097..0d744c526 100644 --- a/server/lib/video-comment.ts +++ b/server/lib/video-comment.ts @@ -1,29 +1,32 @@ import * as Sequelize from 'sequelize' import { ResultList } from '../../shared/models' import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model' +import { AccountModel } from '../models/account/account' import { VideoModel } from '../models/video/video' import { VideoCommentModel } from '../models/video/video-comment' -import { getVideoCommentActivityPubUrl, sendVideoRateChangeToFollowers } from './activitypub' +import { getVideoCommentActivityPubUrl } from './activitypub' import { sendCreateVideoCommentToOrigin, sendCreateVideoCommentToVideoFollowers } from './activitypub/send' async function createVideoComment (obj: { text: string, inReplyToComment: VideoCommentModel, video: VideoModel - accountId: number + account: AccountModel }, t: Sequelize.Transaction) { let originCommentId: number = null + let inReplyToCommentId: number = null if (obj.inReplyToComment) { originCommentId = obj.inReplyToComment.originCommentId || obj.inReplyToComment.id + inReplyToCommentId = obj.inReplyToComment.id } const comment = await VideoCommentModel.create({ text: obj.text, originCommentId, - inReplyToCommentId: obj.inReplyToComment.id, + inReplyToCommentId, videoId: obj.video.id, - accountId: obj.accountId, + accountId: obj.account.id, url: 'fake url' }, { transaction: t, validate: false }) @@ -32,6 +35,7 @@ async function createVideoComment (obj: { const savedComment = await comment.save({ transaction: t }) savedComment.InReplyToVideoComment = obj.inReplyToComment savedComment.Video = obj.video + savedComment.Account = obj.account if (savedComment.Video.isOwned()) { await sendCreateVideoCommentToVideoFollowers(savedComment, t) diff --git a/server/middlewares/validators/pagination.ts b/server/middlewares/validators/pagination.ts index 0895b4eb8..25debfa6e 100644 --- a/server/middlewares/validators/pagination.ts +++ b/server/middlewares/validators/pagination.ts @@ -4,8 +4,8 @@ import { logger } from '../../helpers' import { areValidationErrors } from './utils' const paginationValidator = [ - query('start').optional().isInt().withMessage('Should have a number start'), - query('count').optional().isInt().withMessage('Should have a number count'), + query('start').optional().isInt({ min: 0 }).withMessage('Should have a number start'), + query('count').optional().isInt({ min: 0 }).withMessage('Should have a number count'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking pagination parameters', { parameters: req.query }) diff --git a/server/models/video/video-comment.ts b/server/models/video/video-comment.ts index 25cd6d563..a3e8c48d4 100644 --- a/server/models/video/video-comment.ts +++ b/server/models/video/video-comment.ts @@ -8,18 +8,48 @@ import { VideoComment } from '../../../shared/models/videos/video-comment.model' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub' import { CONSTRAINTS_FIELDS } from '../../initializers' import { AccountModel } from '../account/account' +import { ActorModel } from '../activitypub/actor' +import { ServerModel } from '../server/server' import { getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' enum ScopeNames { WITH_ACCOUNT = 'WITH_ACCOUNT', - WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO' + WITH_IN_REPLY_TO = 'WITH_IN_REPLY_TO', + ATTRIBUTES_FOR_API = 'ATTRIBUTES_FOR_API' } @Scopes({ + [ScopeNames.ATTRIBUTES_FOR_API]: { + attributes: { + include: [ + [ + Sequelize.literal( + '(SELECT COUNT("replies"."id") ' + + 'FROM "videoComment" AS "replies" ' + + 'WHERE "replies"."originCommentId" = "VideoCommentModel"."id")' + ), + 'totalReplies' + ] + ] + } + }, [ScopeNames.WITH_ACCOUNT]: { include: [ - () => AccountModel + { + model: () => AccountModel, + include: [ + { + model: () => ActorModel, + include: [ + { + model: () => ServerModel, + required: false + } + ] + } + ] + } ] }, [ScopeNames.WITH_IN_REPLY_TO]: { @@ -149,7 +179,7 @@ export class VideoCommentModel extends Model { } return VideoCommentModel - .scope([ ScopeNames.WITH_ACCOUNT ]) + .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) .findAndCountAll(query) .then(({ rows, count }) => { return { total: count, data: rows } @@ -169,7 +199,7 @@ export class VideoCommentModel extends Model { } return VideoCommentModel - .scope([ ScopeNames.WITH_ACCOUNT ]) + .scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ]) .findAndCountAll(query) .then(({ rows, count }) => { return { total: count, data: rows } @@ -186,8 +216,10 @@ export class VideoCommentModel extends Model { videoId: this.videoId, createdAt: this.createdAt, updatedAt: this.updatedAt, + totalReplies: this.get('totalReplies') || 0, account: { - name: this.Account.name + name: this.Account.name, + host: this.Account.Actor.getHost() } } as VideoComment } diff --git a/server/tests/api/video-comments.ts b/server/tests/api/video-comments.ts index 2f1e6260a..2c7d1c6e2 100644 --- a/server/tests/api/video-comments.ts +++ b/server/tests/api/video-comments.ts @@ -39,7 +39,18 @@ describe('Test video comments', function () { it('Should create a thread in this video', async function () { const text = 'my super first comment' - await addVideoCommentThread(server.url, server.accessToken, videoUUID, text) + const res = await addVideoCommentThread(server.url, server.accessToken, videoUUID, text) + const comment = res.body + + expect(comment.inReplyToCommentId).to.be.null + expect(comment.text).equal('my super first comment') + expect(comment.videoId).to.equal(videoId) + expect(comment.id).to.equal(comment.threadId) + expect(comment.account.name).to.equal('root') + expect(comment.account.host).to.equal('localhost:9001') + expect(comment.totalReplies).to.equal(0) + expect(dateIsValid(comment.createdAt as string)).to.be.true + expect(dateIsValid(comment.updatedAt as string)).to.be.true }) it('Should list threads of this video', async function () { @@ -55,6 +66,8 @@ describe('Test video comments', function () { expect(comment.videoId).to.equal(videoId) expect(comment.id).to.equal(comment.threadId) expect(comment.account.name).to.equal('root') + expect(comment.account.host).to.equal('localhost:9001') + expect(comment.totalReplies).to.equal(0) expect(dateIsValid(comment.createdAt as string)).to.be.true expect(dateIsValid(comment.updatedAt as string)).to.be.true @@ -120,8 +133,11 @@ describe('Test video comments', function () { expect(res.body.data).to.have.lengthOf(3) expect(res.body.data[0].text).to.equal('my super first comment') + expect(res.body.data[0].totalReplies).to.equal(2) expect(res.body.data[1].text).to.equal('super thread 2') + expect(res.body.data[1].totalReplies).to.equal(1) expect(res.body.data[2].text).to.equal('super thread 3') + expect(res.body.data[2].totalReplies).to.equal(0) }) after(async function () { diff --git a/shared/models/videos/video-comment.model.ts b/shared/models/videos/video-comment.model.ts index 69884782f..d572927c2 100644 --- a/shared/models/videos/video-comment.model.ts +++ b/shared/models/videos/video-comment.model.ts @@ -7,8 +7,10 @@ export interface VideoComment { videoId: number createdAt: Date | string updatedAt: Date | string + totalReplies: number account: { name: string + host: string } }