diff --git a/client/src/app/+videos/+video-watch/video-rate.component.html b/client/src/app/+videos/+video-watch/video-rate.component.html
new file mode 100644
index 000000000..7dd9b3678
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/video-rate.component.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/client/src/app/+videos/+video-watch/video-rate.component.scss b/client/src/app/+videos/+video-watch/video-rate.component.scss
new file mode 100644
index 000000000..f4f696f33
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/video-rate.component.scss
@@ -0,0 +1,15 @@
+@use '_variables' as *;
+@use '_mixins' as *;
+
+.action-button-like,
+.action-button-dislike {
+ filter: brightness(120%);
+
+ .count {
+ margin: 0 5px;
+ }
+}
+
+.activated {
+ color: pvar(--activatedActionButtonColor) !important;
+}
diff --git a/client/src/app/+videos/+video-watch/video-rate.component.ts b/client/src/app/+videos/+video-watch/video-rate.component.ts
new file mode 100644
index 000000000..89a666a62
--- /dev/null
+++ b/client/src/app/+videos/+video-watch/video-rate.component.ts
@@ -0,0 +1,142 @@
+import { Hotkey, HotkeysService } from 'angular2-hotkeys'
+import { Observable } from 'rxjs'
+import { Component, EventEmitter, Input, OnChanges, OnDestroy, OnInit, Output } from '@angular/core'
+import { Notifier, ScreenService } from '@app/core'
+import { VideoDetails, VideoService } from '@app/shared/shared-main'
+import { UserVideoRateType } from '@shared/models'
+
+@Component({
+ selector: 'my-video-rate',
+ templateUrl: './video-rate.component.html',
+ styleUrls: [ './video-rate.component.scss' ]
+})
+export class VideoRateComponent implements OnInit, OnChanges, OnDestroy {
+ @Input() video: VideoDetails
+ @Input() isUserLoggedIn: boolean
+
+ @Output() userRatingLoaded = new EventEmitter()
+ @Output() rateUpdated = new EventEmitter()
+
+ userRating: UserVideoRateType
+
+ tooltipLike = ''
+ tooltipDislike = ''
+
+ private hotkeys: Hotkey[]
+
+ constructor (
+ private videoService: VideoService,
+ private notifier: Notifier,
+ private hotkeysService: HotkeysService,
+ private screenService: ScreenService
+ ) { }
+
+ async ngOnInit () {
+ // Hide the tooltips for unlogged users in mobile view, this adds confusion with the popover
+ if (this.isUserLoggedIn || !this.screenService.isInMobileView()) {
+ this.tooltipLike = $localize`Like this video`
+ this.tooltipDislike = $localize`Dislike this video`
+ }
+
+ if (this.isUserLoggedIn) {
+ this.hotkeys = [
+ new Hotkey('shift+l', () => {
+ this.setLike()
+ return false
+ }, undefined, $localize`Like the video`),
+
+ new Hotkey('shift+d', () => {
+ this.setDislike()
+ return false
+ }, undefined, $localize`Dislike the video`)
+ ]
+
+ this.hotkeysService.add(this.hotkeys)
+ }
+ }
+
+ ngOnChanges () {
+ this.checkUserRating()
+ }
+
+ ngOnDestroy () {
+ this.hotkeysService.remove(this.hotkeys)
+ }
+
+ setLike () {
+ if (this.isUserLoggedIn === false) return
+
+ // Already liked this video
+ if (this.userRating === 'like') this.setRating('none')
+ else this.setRating('like')
+ }
+
+ setDislike () {
+ if (this.isUserLoggedIn === false) return
+
+ // Already disliked this video
+ if (this.userRating === 'dislike') this.setRating('none')
+ else this.setRating('dislike')
+ }
+
+ getRatePopoverText () {
+ if (this.isUserLoggedIn) return undefined
+
+ return $localize`You need to be logged in to rate this video.`
+ }
+
+ private checkUserRating () {
+ // Unlogged users do not have ratings
+ if (this.isUserLoggedIn === false) return
+
+ this.videoService.getUserVideoRating(this.video.id)
+ .subscribe(
+ ratingObject => {
+ if (!ratingObject) return
+
+ this.userRating = ratingObject.rating
+ this.userRatingLoaded.emit(this.userRating)
+ },
+
+ err => this.notifier.error(err.message)
+ )
+ }
+
+ private setRating (nextRating: UserVideoRateType) {
+ const ratingMethods: { [id in UserVideoRateType]: (id: number) => Observable } = {
+ like: this.videoService.setVideoLike,
+ dislike: this.videoService.setVideoDislike,
+ none: this.videoService.unsetVideoLike
+ }
+
+ ratingMethods[nextRating].call(this.videoService, this.video.id)
+ .subscribe(
+ () => {
+ // Update the video like attribute
+ this.updateVideoRating(this.userRating, nextRating)
+ this.userRating = nextRating
+ this.rateUpdated.emit(this.userRating)
+ },
+
+ (err: { message: string }) => this.notifier.error(err.message)
+ )
+ }
+
+ private updateVideoRating (oldRating: UserVideoRateType, newRating: UserVideoRateType) {
+ let likesToIncrement = 0
+ let dislikesToIncrement = 0
+
+ if (oldRating) {
+ if (oldRating === 'like') likesToIncrement--
+ if (oldRating === 'dislike') dislikesToIncrement--
+ }
+
+ if (newRating === 'like') likesToIncrement++
+ if (newRating === 'dislike') dislikesToIncrement++
+
+ this.video.likes += likesToIncrement
+ this.video.dislikes += dislikesToIncrement
+
+ this.video.buildLikeAndDislikePercents()
+ }
+}
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 4e424d95d..a659a7db1 100644
--- a/client/src/app/+videos/+video-watch/video-watch.component.html
+++ b/client/src/app/+videos/+video-watch/video-watch.component.html
@@ -75,30 +75,11 @@
-
-
-
-
-
-
-
+