Add history page on client
authorChocobozzz <me@florianbigard.com>
Tue, 18 Dec 2018 08:31:09 +0000 (09:31 +0100)
committerChocobozzz <me@florianbigard.com>
Tue, 18 Dec 2018 10:35:51 +0000 (11:35 +0100)
13 files changed:
client/src/app/+my-account/my-account-history/my-account-history.component.html [new file with mode: 0644]
client/src/app/+my-account/my-account-history/my-account-history.component.scss [new file with mode: 0644]
client/src/app/+my-account/my-account-history/my-account-history.component.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-routing.module.ts
client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
client/src/app/+my-account/my-account.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/shared/menu/top-menu-dropdown.component.html
client/src/app/shared/menu/top-menu-dropdown.component.scss
client/src/app/shared/menu/top-menu-dropdown.component.ts
client/src/app/shared/shared.module.ts
client/src/app/shared/users/user-history.service.ts [new file with mode: 0644]
server/models/video/video.ts

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 (file)
index 0000000..653b33f
--- /dev/null
@@ -0,0 +1,15 @@
+<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>
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 (file)
index 0000000..115bb0e
--- /dev/null
@@ -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 (file)
index 0000000..5085521
--- /dev/null
@@ -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.')
+  }
+}
index 601e517b47075ea97e8822387bac7c52c5827aab..a2cbeaffc2b9e24719b4f692127eb2a1a125f54e 100644 (file)
@@ -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'
+          }
+        }
       }
     ]
   }
index 2db81a3fe5ff47b4eed031d77aaaf829fbf2b74a..a735562f8a11793e395746b3c5c653b131c10be8 100644 (file)
@@ -97,7 +97,7 @@
   }
 }
 
-@media screen and (max-width: 800px) {
+@media screen and (max-width: $small-view) {
   .video {
     flex-direction: column;
     height: auto;
index d9381ebfa167da6861d7f1448f07d57e3e2e16a7..1bac9547dbd61689a0b199b331b8d1e3c8e2ac03 100644 (file)
@@ -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'
         }
       ]
     }
index 017ebd57d8773500baeb375b55eb0bf699533e5c..c05406438fcbf302c89554a1b687334bbbaa1857 100644 (file)
@@ -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: [
index 2d6d1c4bfe5e39824379efd090887e939c5713f7..d3c896019a02b9c31d4b42d67363159ae8307b9f 100644 (file)
@@ -4,7 +4,10 @@
     <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>
index f3ef8f81466b6173e6f4b546d0b3f4817616bcda..77159532f64cc44196f969c9c51a31dd234afa7c 100644 (file)
@@ -12,3 +12,7 @@
   position: relative;
   top: 2px;
 }
+
+/deep/ .dropdown-menu {
+  margin-top: 0 !important;
+}
index 272b721b2be98eff791f0f51e9dfe58eca04f78d..e859c30dd62dbdb754eab930f6825df3ac96b257 100644 (file)
@@ -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
 
index 9810e948583292f5d720fdf0b62a9796fb2eb58a..4a5d664db350cc4cf11d3be30a885d0956b3bca4 100644 (file)
@@ -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 (file)
index 0000000..9ed25bf
--- /dev/null
@@ -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<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))
+               )
+  }
+}
index 199ea9ea49c866cfce2b991d72e3ae7aa9f0e447..3f282580c5b11b060ae96bcabe64cd9602c66c0d 100644 (file)
@@ -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