Skip to content
Snippets Groups Projects
Unverified Commit 4635f59d authored by Chocobozzz's avatar Chocobozzz
Browse files

Add video comment components

parent ea44f375
No related branches found
No related tags found
No related merge requests found
Showing
with 542 additions and 21 deletions
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;
font-size: 13px;
}
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()
}
}
@import '_variables';
@import '_mixins';
@import 'variables';
@import 'mixins';
.peertube-select-container {
@include peertube-select-container(130px);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment