aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-03-08 21:35:43 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-03-08 21:35:43 +0100
commitd38b82810638b9f664c9016fac2684454c273a77 (patch)
tree9465c367e5033675309efca4d66790c6fdd5230d /client/src
parent8f9064432122cba0f518a24ac4378357dadec589 (diff)
downloadPeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.gz
PeerTube-d38b82810638b9f664c9016fac2684454c273a77.tar.zst
PeerTube-d38b82810638b9f664c9016fac2684454c273a77.zip
Add like/dislike system for videos
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/shared/users/user.service.ts2
-rw-r--r--client/src/app/videos/shared/index.ts1
-rw-r--r--client/src/app/videos/shared/rate-type.type.ts1
-rw-r--r--client/src/app/videos/shared/sort-field.type.ts6
-rw-r--r--client/src/app/videos/shared/video.model.ts8
-rw-r--r--client/src/app/videos/shared/video.service.ts43
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.html20
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.scss28
-rw-r--r--client/src/app/videos/video-watch/video-watch.component.ts70
9 files changed, 167 insertions, 12 deletions
diff --git a/client/src/app/shared/users/user.service.ts b/client/src/app/shared/users/user.service.ts
index 4cf100f0d..865e04d48 100644
--- a/client/src/app/shared/users/user.service.ts
+++ b/client/src/app/shared/users/user.service.ts
@@ -8,7 +8,7 @@ import { RestExtractor } from '../rest';
8 8
9@Injectable() 9@Injectable()
10export class UserService { 10export class UserService {
11 private static BASE_USERS_URL = '/api/v1/users/'; 11 static BASE_USERS_URL = '/api/v1/users/';
12 12
13 constructor( 13 constructor(
14 private authHttp: AuthHttp, 14 private authHttp: AuthHttp,
diff --git a/client/src/app/videos/shared/index.ts b/client/src/app/videos/shared/index.ts
index 67d16ead1..beaa528c0 100644
--- a/client/src/app/videos/shared/index.ts
+++ b/client/src/app/videos/shared/index.ts
@@ -1,4 +1,5 @@
1export * from './loader'; 1export * from './loader';
2export * from './sort-field.type'; 2export * from './sort-field.type';
3export * from './rate-type.type';
3export * from './video.model'; 4export * from './video.model';
4export * from './video.service'; 5export * from './video.service';
diff --git a/client/src/app/videos/shared/rate-type.type.ts b/client/src/app/videos/shared/rate-type.type.ts
new file mode 100644
index 000000000..88034d1ff
--- /dev/null
+++ b/client/src/app/videos/shared/rate-type.type.ts
@@ -0,0 +1 @@
export type RateType = 'like' | 'dislike';
diff --git a/client/src/app/videos/shared/sort-field.type.ts b/client/src/app/videos/shared/sort-field.type.ts
index 74908e344..7bda3112a 100644
--- a/client/src/app/videos/shared/sort-field.type.ts
+++ b/client/src/app/videos/shared/sort-field.type.ts
@@ -1,3 +1,3 @@
1export type SortField = "name" | "-name" 1export type SortField = 'name' | '-name'
2 | "duration" | "-duration" 2 | 'duration' | '-duration'
3 | "createdAt" | "-createdAt"; 3 | 'createdAt' | '-createdAt';
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
index 8e676708b..3eef936eb 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/videos/shared/video.model.ts
@@ -12,6 +12,8 @@ export class Video {
12 tags: string[]; 12 tags: string[];
13 thumbnailPath: string; 13 thumbnailPath: string;
14 views: number; 14 views: number;
15 likes: number;
16 dislikes: number;
15 17
16 private static createByString(author: string, podHost: string) { 18 private static createByString(author: string, podHost: string) {
17 return author + '@' + podHost; 19 return author + '@' + podHost;
@@ -38,7 +40,9 @@ export class Video {
38 podHost: string, 40 podHost: string,
39 tags: string[], 41 tags: string[],
40 thumbnailPath: string, 42 thumbnailPath: string,
41 views: number 43 views: number,
44 likes: number,
45 dislikes: number,
42 }) { 46 }) {
43 this.author = hash.author; 47 this.author = hash.author;
44 this.createdAt = new Date(hash.createdAt); 48 this.createdAt = new Date(hash.createdAt);
@@ -52,6 +56,8 @@ export class Video {
52 this.tags = hash.tags; 56 this.tags = hash.tags;
53 this.thumbnailPath = hash.thumbnailPath; 57 this.thumbnailPath = hash.thumbnailPath;
54 this.views = hash.views; 58 this.views = hash.views;
59 this.likes = hash.likes;
60 this.dislikes = hash.dislikes;
55 61
56 this.by = Video.createByString(hash.author, hash.podHost); 62 this.by = Video.createByString(hash.author, hash.podHost);
57 } 63 }
diff --git a/client/src/app/videos/shared/video.service.ts b/client/src/app/videos/shared/video.service.ts
index 7094d9a34..8bb5a2933 100644
--- a/client/src/app/videos/shared/video.service.ts
+++ b/client/src/app/videos/shared/video.service.ts
@@ -6,8 +6,16 @@ import 'rxjs/add/operator/map';
6 6
7import { Search } from '../../shared'; 7import { Search } from '../../shared';
8import { SortField } from './sort-field.type'; 8import { SortField } from './sort-field.type';
9import { RateType } from './rate-type.type';
9import { AuthService } from '../../core'; 10import { AuthService } from '../../core';
10import { AuthHttp, RestExtractor, RestPagination, RestService, ResultList } from '../../shared'; 11import {
12 AuthHttp,
13 RestExtractor,
14 RestPagination,
15 RestService,
16 ResultList,
17 UserService
18} from '../../shared';
11import { Video } from './video.model'; 19import { Video } from './video.model';
12 20
13@Injectable() 21@Injectable()
@@ -56,14 +64,41 @@ export class VideoService {
56 } 64 }
57 65
58 reportVideo(id: string, reason: string) { 66 reportVideo(id: string, reason: string) {
67 const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
59 const body = { 68 const body = {
60 reason 69 reason
61 }; 70 };
62 const url = VideoService.BASE_VIDEO_URL + id + '/abuse';
63 71
64 return this.authHttp.post(url, body) 72 return this.authHttp.post(url, body)
65 .map(this.restExtractor.extractDataBool) 73 .map(this.restExtractor.extractDataBool)
66 .catch((res) => this.restExtractor.handleError(res)); 74 .catch((res) => this.restExtractor.handleError(res));
75 }
76
77 setVideoLike(id: string) {
78 return this.setVideoRate(id, 'like');
79 }
80
81 setVideoDislike(id: string) {
82 return this.setVideoRate(id, 'dislike');
83 }
84
85 getUserVideoRating(id: string) {
86 const url = UserService.BASE_USERS_URL + '/me/videos/' + id + '/rating';
87
88 return this.authHttp.get(url)
89 .map(this.restExtractor.extractDataGet)
90 .catch((res) => this.restExtractor.handleError(res));
91 }
92
93 private setVideoRate(id: string, rateType: RateType) {
94 const url = VideoService.BASE_VIDEO_URL + id + '/rate';
95 const body = {
96 rating: rateType
97 };
98
99 return this.authHttp.put(url, body)
100 .map(this.restExtractor.extractDataBool)
101 .catch((res) => this.restExtractor.handleError(res));
67 } 102 }
68 103
69 private extractVideos(result: ResultList) { 104 private extractVideos(result: ResultList) {
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 24d741ff9..67094359e 100644
--- a/client/src/app/videos/video-watch/video-watch.component.html
+++ b/client/src/app/videos/video-watch/video-watch.component.html
@@ -32,7 +32,7 @@
32 32
33<div *ngIf="video !== null" id="video-info"> 33<div *ngIf="video !== null" id="video-info">
34 <div class="row" id="video-name-actions"> 34 <div class="row" id="video-name-actions">
35 <div class="col-md-8"> 35 <div class="col-md-6">
36 <div class="row"> 36 <div class="row">
37 <div id="video-name" class="col-md-12"> 37 <div id="video-name" class="col-md-12">
38 {{ video.name }} 38 {{ video.name }}
@@ -52,7 +52,23 @@
52 </div> 52 </div>
53 </div> 53 </div>
54 54
55 <div id="video-actions" class="col-md-4 text-right"> 55 <div id="video-actions" class="col-md-6 text-right">
56 <div id="rates">
57 <button
58 id="likes" class="btn btn-default"
59 [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'like' }" (click)="setLike()"
60 >
61 <span class="glyphicon glyphicon-thumbs-up"></span> {{ video.likes }}
62 </button>
63
64 <button
65 id="dislikes" class="btn btn-default"
66 [ngClass]="{ 'not-interactive-btn': !isUserLoggedIn(), 'activated-btn': userRating === 'dislike' }" (click)="setDislike()"
67 >
68 <span class=" glyphicon glyphicon-thumbs-down"></span> {{ video.dislikes }}
69 </button>
70 </div>
71
56 <button id="share" class="btn btn-default" (click)="showShareModal()"> 72 <button id="share" class="btn btn-default" (click)="showShareModal()">
57 <span class="glyphicon glyphicon-share"></span> Share 73 <span class="glyphicon glyphicon-share"></span> Share
58 </button> 74 </button>
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 0b8af52ce..5f322a194 100644
--- a/client/src/app/videos/video-watch/video-watch.component.scss
+++ b/client/src/app/videos/video-watch/video-watch.component.scss
@@ -47,6 +47,34 @@
47 top: 2px; 47 top: 2px;
48 } 48 }
49 49
50 #rates {
51 display: inline-block;
52 margin-right: 20px;
53
54 // Remove focus style
55 .btn:focus {
56 outline: 0;
57 }
58
59 .activated-btn {
60 color: #333;
61 background-color: #e6e6e6;
62 border-color: #8c8c8c;
63 }
64
65 .not-interactive-btn {
66 cursor: default;
67
68 &:hover, &:focus, &:active {
69 color: #333;
70 background-color: #fff;
71 border-color: #ccc;
72 box-shadow: none;
73 outline: 0;
74 }
75 }
76 }
77
50 #share, #more { 78 #share, #more {
51 font-weight: bold; 79 font-weight: bold;
52 opacity: 0.85; 80 opacity: 0.85;
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 d1abc81bc..ed6b30102 100644
--- a/client/src/app/videos/video-watch/video-watch.component.ts
+++ b/client/src/app/videos/video-watch/video-watch.component.ts
@@ -10,7 +10,7 @@ import { AuthService } from '../../core';
10import { VideoMagnetComponent } from './video-magnet.component'; 10import { VideoMagnetComponent } from './video-magnet.component';
11import { VideoShareComponent } from './video-share.component'; 11import { VideoShareComponent } from './video-share.component';
12import { VideoReportComponent } from './video-report.component'; 12import { VideoReportComponent } from './video-report.component';
13import { Video, VideoService } from '../shared'; 13import { RateType, Video, VideoService } from '../shared';
14import { WebTorrentService } from './webtorrent.service'; 14import { WebTorrentService } from './webtorrent.service';
15 15
16@Component({ 16@Component({
@@ -33,6 +33,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
33 player: VideoJSPlayer; 33 player: VideoJSPlayer;
34 playerElement: Element; 34 playerElement: Element;
35 uploadSpeed: number; 35 uploadSpeed: number;
36 userRating: RateType = null;
36 video: Video = null; 37 video: Video = null;
37 videoNotFound = false; 38 videoNotFound = false;
38 39
@@ -61,6 +62,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
61 this.video = video; 62 this.video = video;
62 this.setOpenGraphTags(); 63 this.setOpenGraphTags();
63 this.loadVideo(); 64 this.loadVideo();
65 this.checkUserRating();
64 }, 66 },
65 error => { 67 error => {
66 this.videoNotFound = true; 68 this.videoNotFound = true;
@@ -136,6 +138,40 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
136 }); 138 });
137 } 139 }
138 140
141 setLike() {
142 if (this.isUserLoggedIn() === false) return;
143 // Already liked this video
144 if (this.userRating === 'like') return;
145
146 this.videoService.setVideoLike(this.video.id)
147 .subscribe(
148 () => {
149 // Update the video like attribute
150 this.updateVideoRating(this.userRating, 'like');
151 this.userRating = 'like';
152 },
153
154 err => this.notificationsService.error('Error', err.text)
155 );
156 }
157
158 setDislike() {
159 if (this.isUserLoggedIn() === false) return;
160 // Already disliked this video
161 if (this.userRating === 'dislike') return;
162
163 this.videoService.setVideoDislike(this.video.id)
164 .subscribe(
165 () => {
166 // Update the video dislike attribute
167 this.updateVideoRating(this.userRating, 'dislike');
168 this.userRating = 'dislike';
169 },
170
171 err => this.notificationsService.error('Error', err.text)
172 );
173 }
174
139 showReportModal(event: Event) { 175 showReportModal(event: Event) {
140 event.preventDefault(); 176 event.preventDefault();
141 this.videoReportModal.show(); 177 this.videoReportModal.show();
@@ -154,6 +190,38 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
154 return this.authService.isLoggedIn(); 190 return this.authService.isLoggedIn();
155 } 191 }
156 192
193 private checkUserRating() {
194 // Unlogged users do not have ratings
195 if (this.isUserLoggedIn() === false) return;
196
197 this.videoService.getUserVideoRating(this.video.id)
198 .subscribe(
199 ratingObject => {
200 if (ratingObject) {
201 this.userRating = ratingObject.rating;
202 }
203 },
204
205 err => this.notificationsService.error('Error', err.text)
206 );
207 }
208
209 private updateVideoRating(oldRating: RateType, newRating: RateType) {
210 let likesToIncrement = 0;
211 let dislikesToIncrement = 0;
212
213 if (oldRating) {
214 if (oldRating === 'like') likesToIncrement--;
215 if (oldRating === 'dislike') dislikesToIncrement--;
216 }
217
218 if (newRating === 'like') likesToIncrement++;
219 if (newRating === 'dislike') dislikesToIncrement++;
220
221 this.video.likes += likesToIncrement;
222 this.video.dislikes += dislikesToIncrement;
223 }
224
157 private loadTooLong() { 225 private loadTooLong() {
158 this.error = true; 226 this.error = true;
159 console.error('The video load seems to be abnormally long.'); 227 console.error('The video load seems to be abnormally long.');