Add video comment components
This commit is contained in:
parent
ea44f375f5
commit
4635f59d7c
|
@ -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.'
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,7 @@
|
||||||
top: -2px;
|
top: -2px;
|
||||||
|
|
||||||
&.icon-edit {
|
&.icon-edit {
|
||||||
background-image: url('../../../assets/images/global/edit.svg');
|
background-image: url('../../../assets/images/global/edit-grey.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.icon-delete-grey {
|
&.icon-delete-grey {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export interface VideoPagination {
|
export interface ComponentPagination {
|
||||||
currentPage: number
|
currentPage: number
|
||||||
itemsPerPage: number
|
itemsPerPage: number
|
||||||
totalItems?: number
|
totalItems?: number
|
|
@ -1,6 +1,7 @@
|
||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import { HttpParams } from '@angular/common/http'
|
import { HttpParams } from '@angular/common/http'
|
||||||
import { SortMeta } from 'primeng/components/common/sortmeta'
|
import { SortMeta } from 'primeng/components/common/sortmeta'
|
||||||
|
import { ComponentPagination } from './component-pagination.model'
|
||||||
|
|
||||||
import { RestPagination } from './rest-pagination'
|
import { RestPagination } from './rest-pagination'
|
||||||
|
|
||||||
|
@ -31,4 +32,10 @@ export class RestService {
|
||||||
return newParams
|
return newParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentPaginationToRestPagination (componentPagination: ComponentPagination): RestPagination {
|
||||||
|
const start: number = (componentPagination.currentPage - 1) * componentPagination.itemsPerPage
|
||||||
|
const count: number = componentPagination.itemsPerPage
|
||||||
|
|
||||||
|
return { start, count }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { Observable } from 'rxjs/Observable'
|
import { Observable } from 'rxjs/Observable'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
|
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||||
import { SortField } from './sort-field.type'
|
import { SortField } from './sort-field.type'
|
||||||
import { VideoPagination } from './video-pagination.model'
|
|
||||||
import { Video } from './video.model'
|
import { Video } from './video.model'
|
||||||
|
|
||||||
export abstract class AbstractVideoList implements OnInit {
|
export abstract class AbstractVideoList implements OnInit {
|
||||||
pagination: VideoPagination = {
|
pagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 25,
|
itemsPerPage: 25,
|
||||||
totalItems: null
|
totalItems: null
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: bold;
|
|
||||||
transition: color 0.2s;
|
transition: color 0.2s;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: $font-semibold;
|
font-weight: $font-semibold;
|
||||||
|
|
|
@ -10,13 +10,13 @@ import { UserVideoRate } from '../../../../../shared/models/videos/user-video-ra
|
||||||
import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
|
import { VideoRateType } from '../../../../../shared/models/videos/video-rate.type'
|
||||||
import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
|
import { VideoUpdate } from '../../../../../shared/models/videos/video-update.model'
|
||||||
import { environment } from '../../../environments/environment'
|
import { environment } from '../../../environments/environment'
|
||||||
|
import { ComponentPagination } from '../rest/component-pagination.model'
|
||||||
import { RestExtractor } from '../rest/rest-extractor.service'
|
import { RestExtractor } from '../rest/rest-extractor.service'
|
||||||
import { RestService } from '../rest/rest.service'
|
import { RestService } from '../rest/rest.service'
|
||||||
import { UserService } from '../users/user.service'
|
import { UserService } from '../users/user.service'
|
||||||
import { SortField } from './sort-field.type'
|
import { SortField } from './sort-field.type'
|
||||||
import { VideoDetails } from './video-details.model'
|
import { VideoDetails } from './video-details.model'
|
||||||
import { VideoEdit } from './video-edit.model'
|
import { VideoEdit } from './video-edit.model'
|
||||||
import { VideoPagination } from './video-pagination.model'
|
|
||||||
import { Video } from './video.model'
|
import { Video } from './video.model'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -71,8 +71,8 @@ export class VideoService {
|
||||||
.catch(this.restExtractor.handleError)
|
.catch(this.restExtractor.handleError)
|
||||||
}
|
}
|
||||||
|
|
||||||
getMyVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
|
getMyVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
|
||||||
const pagination = this.videoPaginationToRestPagination(videoPagination)
|
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
@ -82,8 +82,8 @@ export class VideoService {
|
||||||
.catch((res) => this.restExtractor.handleError(res))
|
.catch((res) => this.restExtractor.handleError(res))
|
||||||
}
|
}
|
||||||
|
|
||||||
getVideos (videoPagination: VideoPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
|
getVideos (videoPagination: ComponentPagination, sort: SortField): Observable<{ videos: Video[], totalVideos: number}> {
|
||||||
const pagination = this.videoPaginationToRestPagination(videoPagination)
|
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
@ -94,10 +94,14 @@ export class VideoService {
|
||||||
.catch((res) => this.restExtractor.handleError(res))
|
.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 url = VideoService.BASE_VIDEO_URL + 'search'
|
||||||
|
|
||||||
const pagination = this.videoPaginationToRestPagination(videoPagination)
|
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
||||||
|
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
params = this.restService.addRestGetParams(params, pagination, sort)
|
params = this.restService.addRestGetParams(params, pagination, sort)
|
||||||
|
@ -139,13 +143,6 @@ export class VideoService {
|
||||||
.catch(res => this.restExtractor.handleError(res))
|
.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) {
|
private setVideoRate (id: number, rateType: VideoRateType) {
|
||||||
const url = VideoService.BASE_VIDEO_URL + id + '/rate'
|
const url = VideoService.BASE_VIDEO_URL + id + '/rate'
|
||||||
const body: UserVideoRateUpdate = {
|
const body: UserVideoRateUpdate = {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<form novalidate [formGroup]="form" (ngSubmit)="formValidated()">
|
||||||
|
<div class="form-group">
|
||||||
|
<textarea placeholder="Add comment..." formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }">
|
||||||
|
</textarea>
|
||||||
|
<div *ngIf="formErrors.text" class="form-error">
|
||||||
|
{{ formErrors.text }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="submit-comment">
|
||||||
|
<button *ngIf="isAddButtonDisplayed()" [ngClass]="{ disabled: !form.valid }">
|
||||||
|
Post comment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<VideoCommentCreate>()
|
||||||
|
|
||||||
|
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<any>
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="comment">
|
||||||
|
<div class="comment-account-date">
|
||||||
|
<div class="comment-account">{{ comment.by }}</div>
|
||||||
|
<div class="comment-date">{{ comment.createdAt | myFromNow }}</div>
|
||||||
|
</div>
|
||||||
|
<div>{{ comment.text }}</div>
|
||||||
|
|
||||||
|
<div class="comment-actions">
|
||||||
|
<div *ngIf="isUserLoggedIn()" (click)="onWantToReply()" class="comment-action-reply">Reply</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<my-video-comment-add
|
||||||
|
*ngIf="isUserLoggedIn() && inReplyToCommentId === comment.id" [video]="video" [parentComment]="comment"
|
||||||
|
(commentCreated)="onCommentReplyCreated($event)"
|
||||||
|
></my-video-comment-add>
|
||||||
|
|
||||||
|
<div *ngIf="commentTree" class="children">
|
||||||
|
<div *ngFor="let commentChild of commentTree.children">
|
||||||
|
<my-video-comment
|
||||||
|
[comment]="commentChild.comment"
|
||||||
|
[video]="video"
|
||||||
|
[inReplyToCommentId]="inReplyToCommentId"
|
||||||
|
[commentTree]="commentChild"
|
||||||
|
(wantedToReply)="onWantedToReply($event)"
|
||||||
|
(resetReply)="onResetReply()"
|
||||||
|
></my-video-comment>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<VideoComment>()
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<VideoCommentThreadTree> {
|
||||||
|
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<VideoCommentServerModel>) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<div>
|
||||||
|
<div class="title-page title-page-single">
|
||||||
|
Comments
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<my-video-comment-add
|
||||||
|
*ngIf="isUserLoggedIn()"
|
||||||
|
[video]="video"
|
||||||
|
(commentCreated)="onCommentThreadCreated($event)"
|
||||||
|
></my-video-comment-add>
|
||||||
|
|
||||||
|
<div class="comment-threads">
|
||||||
|
<div *ngFor="let comment of comments">
|
||||||
|
<my-video-comment
|
||||||
|
[comment]="comment"
|
||||||
|
[video]="video"
|
||||||
|
[inReplyToCommentId]="inReplyToCommentId"
|
||||||
|
[commentTree]="threadComments[comment.id]"
|
||||||
|
(wantedToReply)="onWantedToReply($event)"
|
||||||
|
(resetReply)="onResetReply()"
|
||||||
|
></my-video-comment>
|
||||||
|
|
||||||
|
<div *ngIf="comment.totalReplies !== 0 && !threadComments[comment.id]" (click)="viewReplies(comment)" class="view-replies">
|
||||||
|
View all {{ comment.totalReplies }} replies
|
||||||
|
|
||||||
|
<span *ngIf="!threadLoading[comment.id]" class="glyphicon glyphicon-menu-down"></span>
|
||||||
|
<my-loader class="comment-thread-loading" [loading]="threadLoading[comment.id]"></my-loader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
@import '_variables';
|
@import 'variables';
|
||||||
@import '_mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
.peertube-select-container {
|
.peertube-select-container {
|
||||||
@include peertube-select-container(130px);
|
@include peertube-select-container(130px);
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
||||||
import { ModalDirective } from 'ngx-bootstrap/modal'
|
import { ModalDirective } from 'ngx-bootstrap/modal'
|
||||||
import { VideoDetails } from '../../shared/video/video-details.model'
|
import { VideoDetails } from '../../../shared/video/video-details.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-download',
|
selector: 'my-video-download',
|
|
@ -1,5 +1,5 @@
|
||||||
@import '_variables';
|
@import 'variables';
|
||||||
@import '_mixins';
|
@import 'mixins';
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
@include peertube-textarea(100%, 60px);
|
@include peertube-textarea(100%, 60px);
|
|
@ -2,8 +2,8 @@ import { Component, Input, OnInit, ViewChild } from '@angular/core'
|
||||||
import { FormBuilder, FormGroup } from '@angular/forms'
|
import { FormBuilder, FormGroup } from '@angular/forms'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
import { ModalDirective } from 'ngx-bootstrap/modal'
|
import { ModalDirective } from 'ngx-bootstrap/modal'
|
||||||
import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../shared'
|
import { FormReactive, VIDEO_ABUSE_REASON, VideoAbuseService } from '../../../shared/index'
|
||||||
import { VideoDetails } from '../../shared/video/video-details.model'
|
import { VideoDetails } from '../../../shared/video/video-details.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-report',
|
selector: 'my-video-report',
|
|
@ -3,7 +3,7 @@ import { Component, Input, ViewChild } from '@angular/core'
|
||||||
import { NotificationsService } from 'angular2-notifications'
|
import { NotificationsService } from 'angular2-notifications'
|
||||||
|
|
||||||
import { ModalDirective } from 'ngx-bootstrap/modal'
|
import { ModalDirective } from 'ngx-bootstrap/modal'
|
||||||
import { VideoDetails } from '../../shared/video/video-details.model'
|
import { VideoDetails } from '../../../shared/video/video-details.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-share',
|
selector: 'my-video-share',
|
|
@ -54,6 +54,12 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li *ngIf="isVideoUpdatable()" role="menuitem">
|
||||||
|
<a class="dropdown-item" title="Update this video" href="#" [routerLink]="[ '/videos/edit', video.uuid ]">
|
||||||
|
<span class="icon icon-edit"></span> Update
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li *ngIf="isVideoRemovable()" role="menuitem">
|
<li *ngIf="isVideoRemovable()" role="menuitem">
|
||||||
<a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
|
<a class="dropdown-item" title="Delete this video" href="#" (click)="removeVideo($event)">
|
||||||
<span class="icon icon-blacklist"></span> Delete
|
<span class="icon icon-blacklist"></span> Delete
|
||||||
|
@ -149,6 +155,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<my-video-comments [video]="video" [user]="user"></my-video-comments>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="other-videos">
|
<div class="other-videos">
|
||||||
|
|
|
@ -126,6 +126,10 @@
|
||||||
background-image: url('../../../assets/images/video/download-black.svg');
|
background-image: url('../../../assets/images/video/download-black.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.icon-edit {
|
||||||
|
background-image: url('../../../assets/images/global/edit-black.svg');
|
||||||
|
}
|
||||||
|
|
||||||
&.icon-alert {
|
&.icon-alert {
|
||||||
background-image: url('../../../assets/images/video/alert.svg');
|
background-image: url('../../../assets/images/video/alert.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,9 @@ import { VideoDetails } from '../../shared/video/video-details.model'
|
||||||
import { Video } from '../../shared/video/video.model'
|
import { Video } from '../../shared/video/video.model'
|
||||||
import { VideoService } from '../../shared/video/video.service'
|
import { VideoService } from '../../shared/video/video.service'
|
||||||
import { MarkdownService } from '../shared'
|
import { MarkdownService } from '../shared'
|
||||||
import { VideoDownloadComponent } from './video-download.component'
|
import { VideoDownloadComponent } from './modal/video-download.component'
|
||||||
import { VideoReportComponent } from './video-report.component'
|
import { VideoReportComponent } from './modal/video-report.component'
|
||||||
import { VideoShareComponent } from './video-share.component'
|
import { VideoShareComponent } from './modal/video-share.component'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch',
|
selector: 'my-video-watch',
|
||||||
|
@ -208,6 +208,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
return this.authService.isLoggedIn()
|
return this.authService.isLoggedIn()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isVideoUpdatable () {
|
||||||
|
return this.video.isUpdatableBy(this.authService.getUser())
|
||||||
|
}
|
||||||
|
|
||||||
isVideoBlacklistable () {
|
isVideoBlacklistable () {
|
||||||
return this.video.isBlackistableBy(this.user)
|
return this.video.isBlackistableBy(this.user)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,13 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'
|
||||||
import { ClipboardModule } from 'ngx-clipboard'
|
import { ClipboardModule } from 'ngx-clipboard'
|
||||||
import { SharedModule } from '../../shared'
|
import { SharedModule } from '../../shared'
|
||||||
import { MarkdownService } from '../shared'
|
import { MarkdownService } from '../shared'
|
||||||
import { VideoDownloadComponent } from './video-download.component'
|
import { VideoCommentAddComponent } from './comment/video-comment-add.component'
|
||||||
import { VideoReportComponent } from './video-report.component'
|
import { VideoCommentComponent } from './comment/video-comment.component'
|
||||||
import { VideoShareComponent } from './video-share.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'
|
import { VideoWatchRoutingModule } from './video-watch-routing.module'
|
||||||
|
|
||||||
|
@ -24,7 +28,10 @@ import { VideoWatchComponent } from './video-watch.component'
|
||||||
|
|
||||||
VideoDownloadComponent,
|
VideoDownloadComponent,
|
||||||
VideoShareComponent,
|
VideoShareComponent,
|
||||||
VideoReportComponent
|
VideoReportComponent,
|
||||||
|
VideoCommentsComponent,
|
||||||
|
VideoCommentAddComponent,
|
||||||
|
VideoCommentComponent
|
||||||
],
|
],
|
||||||
|
|
||||||
exports: [
|
exports: [
|
||||||
|
@ -32,7 +39,8 @@ import { VideoWatchComponent } from './video-watch.component'
|
||||||
],
|
],
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
MarkdownService
|
MarkdownService,
|
||||||
|
VideoCommentService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class VideoWatchModule { }
|
export class VideoWatchModule { }
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>edit</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<defs></defs>
|
||||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
|
<g id="Artboard-4" transform="translate(-48.000000, -203.000000)" stroke="#000000" stroke-width="2">
|
||||||
|
<g id="41" transform="translate(48.000000, 203.000000)">
|
||||||
|
<path d="M3,21.0000003 L3,17 L15.8898356,4.11016442 C17.0598483,2.9401517 18.9638992,2.94723715 20.1306896,4.11402752 L19.9181432,3.90148112 C21.0902894,5.07362738 21.0882407,6.97202708 19.9174652,8.1377941 L7,21.0000003 L3,21.0000003 Z" id="Path-74" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||||
|
<path d="M14.5,5.5 L18.5,9.5" id="Path-75"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
@ -118,10 +118,7 @@ label {
|
||||||
|
|
||||||
// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
|
// Thanks https://gist.github.com/alexandrevicenzi/680147013e902a4eaa5d
|
||||||
.glyphicon-refresh-animate {
|
.glyphicon-refresh-animate {
|
||||||
-animation: spin .7s infinite linear;
|
animation: spin .7s infinite linear;
|
||||||
-ms-animation: spin .7s infinite linear;
|
|
||||||
-webkit-animation: spinw .7s infinite linear;
|
|
||||||
-moz-animation: spinm .7s infinite linear;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
|
@ -129,16 +126,6 @@ label {
|
||||||
to { transform: scale(1) rotate(360deg);}
|
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
|
// ngprime data table customizations
|
||||||
p-datatable {
|
p-datatable {
|
||||||
font-size: 15px !important;
|
font-size: 15px !important;
|
||||||
|
|
|
@ -151,7 +151,7 @@ app.use(function (req, res, next) {
|
||||||
})
|
})
|
||||||
|
|
||||||
app.use(function (err, req, res, next) {
|
app.use(function (err, req, res, next) {
|
||||||
logger.error(err)
|
logger.error(err, err)
|
||||||
res.sendStatus(err.status || 500)
|
res.sendStatus(err.status || 500)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -66,9 +66,7 @@ async function addVideoCommentThreadRetryWrapper (req: express.Request, res: exp
|
||||||
const comment = await retryTransactionWrapper(addVideoCommentThread, options)
|
const comment = await retryTransactionWrapper(addVideoCommentThread, options)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
comment: {
|
comment: comment.toFormattedJSON()
|
||||||
id: comment.id
|
|
||||||
}
|
|
||||||
}).end()
|
}).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,7 +78,7 @@ function addVideoCommentThread (req: express.Request, res: express.Response) {
|
||||||
text: videoCommentInfo.text,
|
text: videoCommentInfo.text,
|
||||||
inReplyToComment: null,
|
inReplyToComment: null,
|
||||||
video: res.locals.video,
|
video: res.locals.video,
|
||||||
accountId: res.locals.oauth.token.User.Account.id
|
account: res.locals.oauth.token.User.Account
|
||||||
}, t)
|
}, t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -94,9 +92,7 @@ async function addVideoCommentReplyRetryWrapper (req: express.Request, res: expr
|
||||||
const comment = await retryTransactionWrapper(addVideoCommentReply, options)
|
const comment = await retryTransactionWrapper(addVideoCommentReply, options)
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
comment: {
|
comment: comment.toFormattedJSON()
|
||||||
id: comment.id
|
|
||||||
}
|
|
||||||
}).end()
|
}).end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +104,7 @@ function addVideoCommentReply (req: express.Request, res: express.Response, next
|
||||||
text: videoCommentInfo.text,
|
text: videoCommentInfo.text,
|
||||||
inReplyToComment: res.locals.videoComment,
|
inReplyToComment: res.locals.videoComment,
|
||||||
video: res.locals.video,
|
video: res.locals.video,
|
||||||
accountId: res.locals.oauth.token.User.Account.id
|
account: res.locals.oauth.token.User.Account
|
||||||
}, t)
|
}, t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
import * as Sequelize from 'sequelize'
|
import * as Sequelize from 'sequelize'
|
||||||
import { ResultList } from '../../shared/models'
|
import { ResultList } from '../../shared/models'
|
||||||
import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
|
import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
|
||||||
|
import { AccountModel } from '../models/account/account'
|
||||||
import { VideoModel } from '../models/video/video'
|
import { VideoModel } from '../models/video/video'
|
||||||
import { VideoCommentModel } from '../models/video/video-comment'
|
import { VideoCommentModel } from '../models/video/video-comment'
|
||||||
import { getVideoCommentActivityPubUrl, sendVideoRateChangeToFollowers } from './activitypub'
|
import { getVideoCommentActivityPubUrl } from './activitypub'
|
||||||
import { sendCreateVideoCommentToOrigin, sendCreateVideoCommentToVideoFollowers } from './activitypub/send'
|
import { sendCreateVideoCommentToOrigin, sendCreateVideoCommentToVideoFollowers } from './activitypub/send'
|
||||||
|
|
||||||
async function createVideoComment (obj: {
|
async function createVideoComment (obj: {
|
||||||
text: string,
|
text: string,
|
||||||
inReplyToComment: VideoCommentModel,
|
inReplyToComment: VideoCommentModel,
|
||||||
video: VideoModel
|
video: VideoModel
|
||||||
accountId: number
|
account: AccountModel
|
||||||
}, t: Sequelize.Transaction) {
|
}, t: Sequelize.Transaction) {
|
||||||
let originCommentId: number = null
|
let originCommentId: number = null
|
||||||
|
let inReplyToCommentId: number = null
|
||||||
|
|
||||||
if (obj.inReplyToComment) {
|
if (obj.inReplyToComment) {
|
||||||
originCommentId = obj.inReplyToComment.originCommentId || obj.inReplyToComment.id
|
originCommentId = obj.inReplyToComment.originCommentId || obj.inReplyToComment.id
|
||||||
|
inReplyToCommentId = obj.inReplyToComment.id
|
||||||
}
|
}
|
||||||
|
|
||||||
const comment = await VideoCommentModel.create({
|
const comment = await VideoCommentModel.create({
|
||||||
text: obj.text,
|
text: obj.text,
|
||||||
originCommentId,
|
originCommentId,
|
||||||
inReplyToCommentId: obj.inReplyToComment.id,
|
inReplyToCommentId,
|
||||||
videoId: obj.video.id,
|
videoId: obj.video.id,
|
||||||
accountId: obj.accountId,
|
accountId: obj.account.id,
|
||||||
url: 'fake url'
|
url: 'fake url'
|
||||||
}, { transaction: t, validate: false })
|
}, { transaction: t, validate: false })
|
||||||
|
|
||||||
|
@ -32,6 +35,7 @@ async function createVideoComment (obj: {
|
||||||
const savedComment = await comment.save({ transaction: t })
|
const savedComment = await comment.save({ transaction: t })
|
||||||
savedComment.InReplyToVideoComment = obj.inReplyToComment
|
savedComment.InReplyToVideoComment = obj.inReplyToComment
|
||||||
savedComment.Video = obj.video
|
savedComment.Video = obj.video
|
||||||
|
savedComment.Account = obj.account
|
||||||
|
|
||||||
if (savedComment.Video.isOwned()) {
|
if (savedComment.Video.isOwned()) {
|
||||||
await sendCreateVideoCommentToVideoFollowers(savedComment, t)
|
await sendCreateVideoCommentToVideoFollowers(savedComment, t)
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { logger } from '../../helpers'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from './utils'
|
||||||
|
|
||||||
const paginationValidator = [
|
const paginationValidator = [
|
||||||
query('start').optional().isInt().withMessage('Should have a number start'),
|
query('start').optional().isInt({ min: 0 }).withMessage('Should have a number start'),
|
||||||
query('count').optional().isInt().withMessage('Should have a number count'),
|
query('count').optional().isInt({ min: 0 }).withMessage('Should have a number count'),
|
||||||
|
|
||||||
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking pagination parameters', { parameters: req.query })
|
logger.debug('Checking pagination parameters', { parameters: req.query })
|
||||||
|
|
|
@ -8,18 +8,48 @@ import { VideoComment } from '../../../shared/models/videos/video-comment.model'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
||||||
import { AccountModel } from '../account/account'
|
import { AccountModel } from '../account/account'
|
||||||
|
import { ActorModel } from '../activitypub/actor'
|
||||||
|
import { ServerModel } from '../server/server'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
|
|
||||||
enum ScopeNames {
|
enum ScopeNames {
|
||||||
WITH_ACCOUNT = 'WITH_ACCOUNT',
|
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({
|
@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]: {
|
[ScopeNames.WITH_ACCOUNT]: {
|
||||||
include: [
|
include: [
|
||||||
() => AccountModel
|
{
|
||||||
|
model: () => AccountModel,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => ActorModel,
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: () => ServerModel,
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
[ScopeNames.WITH_IN_REPLY_TO]: {
|
[ScopeNames.WITH_IN_REPLY_TO]: {
|
||||||
|
@ -149,7 +179,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoCommentModel
|
return VideoCommentModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT ])
|
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
|
@ -169,7 +199,7 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoCommentModel
|
return VideoCommentModel
|
||||||
.scope([ ScopeNames.WITH_ACCOUNT ])
|
.scope([ ScopeNames.WITH_ACCOUNT, ScopeNames.ATTRIBUTES_FOR_API ])
|
||||||
.findAndCountAll(query)
|
.findAndCountAll(query)
|
||||||
.then(({ rows, count }) => {
|
.then(({ rows, count }) => {
|
||||||
return { total: count, data: rows }
|
return { total: count, data: rows }
|
||||||
|
@ -186,8 +216,10 @@ export class VideoCommentModel extends Model<VideoCommentModel> {
|
||||||
videoId: this.videoId,
|
videoId: this.videoId,
|
||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
updatedAt: this.updatedAt,
|
updatedAt: this.updatedAt,
|
||||||
|
totalReplies: this.get('totalReplies') || 0,
|
||||||
account: {
|
account: {
|
||||||
name: this.Account.name
|
name: this.Account.name,
|
||||||
|
host: this.Account.Actor.getHost()
|
||||||
}
|
}
|
||||||
} as VideoComment
|
} as VideoComment
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,18 @@ describe('Test video comments', function () {
|
||||||
it('Should create a thread in this video', async function () {
|
it('Should create a thread in this video', async function () {
|
||||||
const text = 'my super first comment'
|
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 () {
|
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.videoId).to.equal(videoId)
|
||||||
expect(comment.id).to.equal(comment.threadId)
|
expect(comment.id).to.equal(comment.threadId)
|
||||||
expect(comment.account.name).to.equal('root')
|
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.createdAt as string)).to.be.true
|
||||||
expect(dateIsValid(comment.updatedAt 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).to.have.lengthOf(3)
|
||||||
|
|
||||||
expect(res.body.data[0].text).to.equal('my super first comment')
|
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].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].text).to.equal('super thread 3')
|
||||||
|
expect(res.body.data[2].totalReplies).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
after(async function () {
|
after(async function () {
|
||||||
|
|
|
@ -7,8 +7,10 @@ export interface VideoComment {
|
||||||
videoId: number
|
videoId: number
|
||||||
createdAt: Date | string
|
createdAt: Date | string
|
||||||
updatedAt: Date | string
|
updatedAt: Date | string
|
||||||
|
totalReplies: number
|
||||||
account: {
|
account: {
|
||||||
name: string
|
name: string
|
||||||
|
host: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue