Verified Commit 4635f59d authored by Chocobozzz's avatar Chocobozzz
Browse files

Add video comment components

parent ea44f375
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;
&.icon-edit {
background-image: url('../../../assets/images/global/edit.svg');
background-image: url('../../../assets/images/global/edit-grey.svg');
}
&.icon-delete-grey {
......
export interface VideoPagination {
export interface ComponentPagination {
currentPage: number
itemsPerPage: number
totalItems?: number
......
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 }
}
}
......@@ -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
......
......@@ -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;
......
......@@ -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 = {
......
<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>
@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
}
}
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)
}
}
<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>
@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;
}
}
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()
}
}
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)
}
}
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
}
}
<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>
@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;</