--- /dev/null
+<div i18n *ngIf="pagination.totalItems === 0">You don't have history yet.</div>
+
+<div myInfiniteScroller (nearOfBottom)="onNearOfBottom()" class="videos" #videosElement>
+ <div *ngFor="let videos of videoPages;" class="videos-page">
+ <div class="video" *ngFor="let video of videos">
+ <my-video-thumbnail [video]="video"></my-video-thumbnail>
+
+ <div class="video-info">
+ <a tabindex="-1" class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
+ <span i18n class="video-info-date-views">{{ video.views | myNumberFormatter }} views</span>
+ <a tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+@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;
+ }
+ }
+}
--- /dev/null
+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.')
+ }
+}
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 = [
{
title: 'Muted instances'
}
}
+ },
+ {
+ path: 'history/videos',
+ component: MyAccountHistoryComponent,
+ data: {
+ meta: {
+ title: 'Videos history'
+ }
+ }
}
]
}
}
}
-@media screen and (max-width: 800px) {
+@media screen and (max-width: $small-view) {
.video {
flex-direction: column;
height: auto;
children: [
{
label: this.i18n('My channels'),
- routerLink: '/my-account/videos'
+ routerLink: '/my-account/video-channels'
},
{
label: this.i18n('My videos'),
{
label: this.i18n('My subscriptions'),
routerLink: '/my-account/subscriptions'
+ },
+ {
+ label: this.i18n('My history'),
+ routerLink: '/my-account/history/videos'
}
]
}
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: [
MyAccountDangerZoneComponent,
MyAccountSubscriptionsComponent,
MyAccountBlocklistComponent,
- MyAccountServerBlocklistComponent
+ MyAccountServerBlocklistComponent,
+ MyAccountHistoryComponent
],
exports: [
<a *ngIf="menuEntry.routerLink" [routerLink]="menuEntry.routerLink" routerLinkActive="active" class="title-page">{{ menuEntry.label }}</a>
<div *ngIf="!menuEntry.routerLink" ngbDropdown class="parent-entry" #dropdown="ngbDropdown" (mouseleave)="closeDropdownIfHovered(dropdown)">
- <span (mouseenter)="openDropdownOnHover(dropdown)" role="button" class="title-page" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownToggle>
+ <span
+ (mouseenter)="openDropdownOnHover(dropdown)" [ngClass]="{ active: !!suffixLabels[menuEntry.label] }" ngbDropdownAnchor
+ (click)="dropdownAnchorClicked(dropdown)" role="button" class="title-page"
+ >
<ng-container i18n>{{ menuEntry.label }}</ng-container>
<ng-container *ngIf="!!suffixLabels[menuEntry.label]"> - {{ suffixLabels[menuEntry.label] }}</ng-container>
</span>
position: relative;
top: 2px;
}
+
+/deep/ .dropdown-menu {
+ margin-top: 0 !important;
+}
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
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))
}
.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
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: [
VideoChangeOwnershipValidatorsService,
VideoAcceptOwnershipValidatorsService,
BlocklistService,
+ UserHistoryService,
I18nPrimengCalendarService,
ScreenService,
--- /dev/null
+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<ResultList<Video>>(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))
+ )
+ }
+}
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