diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.html b/client/src/app/+my-account/my-account-history/my-account-history.component.html
new file mode 100644
index 000000000..653b33f89
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.html
@@ -0,0 +1,15 @@
+
You don't have history yet.
+
+
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.scss b/client/src/app/+my-account/my-account-history/my-account-history.component.scss
new file mode 100644
index 000000000..115bb0e5c
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.scss
@@ -0,0 +1,68 @@
+@import '_variables';
+@import '_mixins';
+
+.video {
+ @include row-blocks;
+
+ my-video-thumbnail {
+ margin-right: 10px;
+ }
+
+ .video-info {
+ flex-grow: 1;
+
+ .video-info-name {
+ @include disable-default-a-behaviour;
+
+ color: var(--mainForegroundColor);
+ display: block;
+ width: fit-content;
+ font-size: 18px;
+ font-weight: $font-semibold;
+ }
+
+ .video-info-date-views {
+ font-size: 14px;
+ }
+
+ .video-info-account {
+ @include disable-default-a-behaviour;
+
+ display: block;
+ width: fit-content;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 14px;
+ color: #585858;
+
+ &:hover {
+ color: #303030;
+ }
+ }
+ }
+}
+
+@media screen and (max-width: $small-view) {
+ .video {
+ flex-direction: column;
+ height: auto;
+ text-align: center;
+
+ .video-info-name {
+ margin: auto;
+ }
+
+ input[type=checkbox] {
+ display: none;
+ }
+
+ my-video-thumbnail {
+ margin-right: 0;
+ }
+
+ .video-buttons {
+ margin-top: 10px;
+ }
+ }
+}
diff --git a/client/src/app/+my-account/my-account-history/my-account-history.component.ts b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
new file mode 100644
index 000000000..508552167
--- /dev/null
+++ b/client/src/app/+my-account/my-account-history/my-account-history.component.ts
@@ -0,0 +1,66 @@
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { Location } from '@angular/common'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
+import { NotificationsService } from 'angular2-notifications'
+import { AuthService } from '../../core/auth'
+import { ConfirmService } from '../../core/confirm'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ScreenService } from '@app/shared/misc/screen.service'
+import { UserHistoryService } from '@app/shared/users/user-history.service'
+
+@Component({
+ selector: 'my-account-history',
+ templateUrl: './my-account-history.component.html',
+ styleUrls: [ './my-account-history.component.scss' ]
+})
+export class MyAccountHistoryComponent extends AbstractVideoList implements OnInit, OnDestroy {
+ titlePage: string
+ currentRoute = '/my-account/history/videos'
+ pagination: ComponentPagination = {
+ currentPage: 1,
+ itemsPerPage: 5,
+ totalItems: null
+ }
+
+ protected baseVideoWidth = -1
+ protected baseVideoHeight = 155
+
+ constructor (
+ protected router: Router,
+ protected route: ActivatedRoute,
+ protected authService: AuthService,
+ protected notificationsService: NotificationsService,
+ protected location: Location,
+ protected screenService: ScreenService,
+ protected i18n: I18n,
+ private confirmService: ConfirmService,
+ private videoService: VideoService,
+ private userHistoryService: UserHistoryService
+ ) {
+ super()
+
+ this.titlePage = this.i18n('My videos history')
+ }
+
+ ngOnInit () {
+ super.ngOnInit()
+ }
+
+ ngOnDestroy () {
+ super.ngOnDestroy()
+ }
+
+ getVideosObservable (page: number) {
+ const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+ return this.userHistoryService.getUserVideosHistory(newPagination)
+ }
+
+ generateSyndicationList () {
+ throw new Error('Method not implemented.')
+ }
+}
diff --git a/client/src/app/+my-account/my-account-routing.module.ts b/client/src/app/+my-account/my-account-routing.module.ts
index 601e517b4..a2cbeaffc 100644
--- a/client/src/app/+my-account/my-account-routing.module.ts
+++ b/client/src/app/+my-account/my-account-routing.module.ts
@@ -13,6 +13,7 @@ import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-sub
import { MyAccountOwnershipComponent } from '@app/+my-account/my-account-ownership/my-account-ownership.component'
import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
+import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
const myAccountRoutes: Routes = [
{
@@ -114,6 +115,15 @@ const myAccountRoutes: Routes = [
title: 'Muted instances'
}
}
+ },
+ {
+ path: 'history/videos',
+ component: MyAccountHistoryComponent,
+ data: {
+ meta: {
+ title: 'Videos history'
+ }
+ }
}
]
}
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index 2db81a3fe..a735562f8 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -97,7 +97,7 @@
}
}
-@media screen and (max-width: 800px) {
+@media screen and (max-width: $small-view) {
.video {
flex-direction: column;
height: auto;
diff --git a/client/src/app/+my-account/my-account.component.ts b/client/src/app/+my-account/my-account.component.ts
index d9381ebfa..1bac9547d 100644
--- a/client/src/app/+my-account/my-account.component.ts
+++ b/client/src/app/+my-account/my-account.component.ts
@@ -21,7 +21,7 @@ export class MyAccountComponent {
children: [
{
label: this.i18n('My channels'),
- routerLink: '/my-account/videos'
+ routerLink: '/my-account/video-channels'
},
{
label: this.i18n('My videos'),
@@ -30,6 +30,10 @@ export class MyAccountComponent {
{
label: this.i18n('My subscriptions'),
routerLink: '/my-account/subscriptions'
+ },
+ {
+ label: this.i18n('My history'),
+ routerLink: '/my-account/history/videos'
}
]
}
diff --git a/client/src/app/+my-account/my-account.module.ts b/client/src/app/+my-account/my-account.module.ts
index 017ebd57d..c05406438 100644
--- a/client/src/app/+my-account/my-account.module.ts
+++ b/client/src/app/+my-account/my-account.module.ts
@@ -21,6 +21,7 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin
import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
import { MyAccountBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-blocklist.component'
import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-blocklist/my-account-server-blocklist.component'
+import { MyAccountHistoryComponent } from '@app/+my-account/my-account-history/my-account-history.component'
@NgModule({
imports: [
@@ -49,7 +50,8 @@ import { MyAccountServerBlocklistComponent } from '@app/+my-account/my-account-b
MyAccountDangerZoneComponent,
MyAccountSubscriptionsComponent,
MyAccountBlocklistComponent,
- MyAccountServerBlocklistComponent
+ MyAccountServerBlocklistComponent,
+ MyAccountHistoryComponent
],
exports: [
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.html b/client/src/app/shared/menu/top-menu-dropdown.component.html
index 2d6d1c4bf..d3c896019 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.html
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.html
@@ -4,7 +4,10 @@
{{ menuEntry.label }}
-
+
{{ menuEntry.label }}
- {{ suffixLabels[menuEntry.label] }}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.scss b/client/src/app/shared/menu/top-menu-dropdown.component.scss
index f3ef8f814..77159532f 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.scss
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.scss
@@ -12,3 +12,7 @@
position: relative;
top: 2px;
}
+
+/deep/ .dropdown-menu {
+ margin-top: 0 !important;
+}
diff --git a/client/src/app/shared/menu/top-menu-dropdown.component.ts b/client/src/app/shared/menu/top-menu-dropdown.component.ts
index 272b721b2..e859c30dd 100644
--- a/client/src/app/shared/menu/top-menu-dropdown.component.ts
+++ b/client/src/app/shared/menu/top-menu-dropdown.component.ts
@@ -1,9 +1,8 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'
import { filter, take } from 'rxjs/operators'
-import { NavigationStart, Router } from '@angular/router'
+import { NavigationEnd, Router } from '@angular/router'
import { Subscription } from 'rxjs'
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
-import { drop } from 'lodash-es'
export type TopMenuDropdownParam = {
label: string
@@ -34,7 +33,7 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
this.updateChildLabels(window.location.pathname)
this.routeSub = this.router.events
- .pipe(filter(event => event instanceof NavigationStart))
+ .pipe(filter(event => event instanceof NavigationEnd))
.subscribe(() => this.updateChildLabels(window.location.pathname))
}
@@ -52,6 +51,15 @@ export class TopMenuDropdownComponent implements OnInit, OnDestroy {
.subscribe(e => this.openedOnHover = false)
}
+ dropdownAnchorClicked (dropdown: NgbDropdown) {
+ if (this.openedOnHover) {
+ this.openedOnHover = false
+ return
+ }
+
+ return dropdown.toggle()
+ }
+
closeDropdownIfHovered (dropdown: NgbDropdown) {
if (this.openedOnHover === false) return
diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts
index 9810e9485..4a5d664db 100644
--- a/client/src/app/shared/shared.module.ts
+++ b/client/src/app/shared/shared.module.ts
@@ -62,6 +62,7 @@ import { UserBanModalComponent } from '@app/shared/moderation'
import { UserModerationDropdownComponent } from '@app/shared/moderation/user-moderation-dropdown.component'
import { BlocklistService } from '@app/shared/blocklist'
import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.component'
+import { UserHistoryService } from '@app/shared/users/user-history.service'
@NgModule({
imports: [
@@ -181,6 +182,7 @@ import { TopMenuDropdownComponent } from '@app/shared/menu/top-menu-dropdown.com
VideoChangeOwnershipValidatorsService,
VideoAcceptOwnershipValidatorsService,
BlocklistService,
+ UserHistoryService,
I18nPrimengCalendarService,
ScreenService,
diff --git a/client/src/app/shared/users/user-history.service.ts b/client/src/app/shared/users/user-history.service.ts
new file mode 100644
index 000000000..9ed25bfc7
--- /dev/null
+++ b/client/src/app/shared/users/user-history.service.ts
@@ -0,0 +1,45 @@
+import { HttpClient, HttpParams } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { environment } from '../../../environments/environment'
+import { RestExtractor } from '../rest/rest-extractor.service'
+import { RestService } from '../rest/rest.service'
+import { Video } from '../video/video.model'
+import { catchError, map, switchMap } from 'rxjs/operators'
+import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
+import { VideoService } from '@app/shared/video/video.service'
+import { ResultList } from '../../../../../shared'
+
+@Injectable()
+export class UserHistoryService {
+ static BASE_USER_VIDEOS_HISTORY_URL = environment.apiUrl + '/api/v1/users/me/history/videos'
+
+ constructor (
+ private authHttp: HttpClient,
+ private restExtractor: RestExtractor,
+ private restService: RestService,
+ private videoService: VideoService
+ ) {}
+
+ getUserVideosHistory (historyPagination: ComponentPagination) {
+ const pagination = this.restService.componentPaginationToRestPagination(historyPagination)
+
+ let params = new HttpParams()
+ params = this.restService.addRestGetParams(params, pagination)
+
+ return this.authHttp
+ .get>(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL, { params })
+ .pipe(
+ switchMap(res => this.videoService.extractVideos(res)),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+
+ deleteUserVideosHistory () {
+ return this.authHttp
+ .post(UserHistoryService.BASE_USER_VIDEOS_HISTORY_URL + '/remove', {})
+ .pipe(
+ map(() => this.restExtractor.extractDataBool()),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 199ea9ea4..3f282580c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -425,6 +425,11 @@ type AvailableForListIDsOptions = {
userId: options.historyOfUser.id
}
})
+
+ // Even if the relation is n:m, we know that a user only have 0..1 video history
+ // So we won't have multiple rows for the same video
+ // Without this, we would not be able to sort on "updatedAt" column of UserVideoHistoryModel
+ query.subQuery = false
}
return query