Add local user subscriptions
authorChocobozzz <me@florianbigard.com>
Tue, 21 Aug 2018 14:18:59 +0000 (16:18 +0200)
committerChocobozzz <me@florianbigard.com>
Mon, 27 Aug 2018 07:41:54 +0000 (09:41 +0200)
42 files changed:
client/src/app/+accounts/account-video-channels/account-video-channels.component.scss
client/src/app/+my-account/my-account-routing.module.ts
client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html [new file with mode: 0644]
client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss [new file with mode: 0644]
client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts [new file with mode: 0644]
client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
client/src/app/+my-account/my-account-video-channels/my-account-video-channels.component.scss
client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
client/src/app/+my-account/my-account.component.html
client/src/app/+my-account/my-account.module.ts
client/src/app/+my-account/shared/actor-avatar-info.component.scss
client/src/app/+video-channels/video-channels.component.html
client/src/app/+video-channels/video-channels.component.scss
client/src/app/menu/menu.component.html
client/src/app/menu/menu.component.scss
client/src/app/shared/shared.module.ts
client/src/app/shared/user-subscription/index.ts [new file with mode: 0644]
client/src/app/shared/user-subscription/subscribe-button.component.html [new file with mode: 0644]
client/src/app/shared/user-subscription/subscribe-button.component.scss [new file with mode: 0644]
client/src/app/shared/user-subscription/subscribe-button.component.ts [new file with mode: 0644]
client/src/app/shared/user-subscription/user-subscription.service.ts [new file with mode: 0644]
client/src/app/shared/video-channel/video-channel.service.ts
client/src/app/shared/video/abstract-video-list.html
client/src/app/shared/video/abstract-video-list.ts
client/src/app/shared/video/video-details.model.ts
client/src/app/shared/video/video-miniature.component.html
client/src/app/shared/video/video-miniature.component.scss
client/src/app/shared/video/video-miniature.component.ts
client/src/app/shared/video/video.model.ts
client/src/app/shared/video/video.service.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
client/src/app/videos/video-list/video-user-subscriptions.component.ts [new file with mode: 0644]
client/src/app/videos/videos-routing.module.ts
client/src/app/videos/videos.module.ts
client/src/assets/images/menu/podcasts.svg [new file with mode: 0644]
client/src/assets/images/menu/subscriptions.svg [new file with mode: 0644]
client/src/sass/application.scss
client/src/sass/include/_mixins.scss
client/src/sass/include/_variables.scss
server/lib/job-queue/handlers/activitypub-follow.ts
server/models/activitypub/actor-follow.ts

index c9c7fa8ebed28fbdf059633f51c8baa240e2d7e4..39c0840e4b4e82dcb9fe9f55dbac8f93c208a560 100644 (file)
@@ -2,7 +2,7 @@
 @import '_mixins';
 
 .row {
-  text-align: center;
+  justify-content: center;
 }
 
 a.video-channel {
index 6f0806e8a0f16af9995e9071ea22da5a5f88d480..c1c9791519af2776173d0f7dd025ab61f4d5bcf0 100644 (file)
@@ -9,6 +9,7 @@ import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-vid
 import { MyAccountVideoChannelCreateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-create.component'
 import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channel-update.component'
 import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
+import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
 
 const myAccountRoutes: Routes = [
   {
@@ -74,6 +75,15 @@ const myAccountRoutes: Routes = [
             title: 'Account video imports'
           }
         }
+      },
+      {
+        path: 'subscriptions',
+        component: MyAccountSubscriptionsComponent,
+        data: {
+          meta: {
+            title: 'Account subscriptions'
+          }
+        }
       }
     ]
   }
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.html
new file mode 100644 (file)
index 0000000..4c68cd1
--- /dev/null
@@ -0,0 +1,23 @@
+<div class="video-channels">
+  <div *ngFor="let videoChannel of videoChannels" class="video-channel">
+    <a [routerLink]="[ '/video-channels', videoChannel.name ]">
+      <img [src]="videoChannel.avatarUrl" alt="Avatar" />
+    </a>
+
+    <div class="video-channel-info">
+      <a [routerLink]="[ '/video-channels', videoChannel.name ]" class="video-channel-names" i18n-title title="Go to the channel">
+        <div class="video-channel-display-name">{{ videoChannel.displayName }}</div>
+        <div class="video-channel-name">{{ videoChannel.name }}</div>
+      </a>
+
+      <div i18n class="video-channel-followers">{{ videoChannel.followersCount }} subscribers</div>
+
+      <a [routerLink]="[ '/accounts', videoChannel.ownerBy ]" i18n-title title="Go the owner account page" class="actor-owner">
+        <span i18n>Created by {{ videoChannel.ownerBy }}</span>
+        <img [src]="videoChannel.ownerAvatarUrl" alt="Owner account avatar" />
+      </a>
+    </div>
+
+    <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button>
+  </div>
+</div>
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.scss
new file mode 100644 (file)
index 0000000..2fbfa33
--- /dev/null
@@ -0,0 +1,49 @@
+@import '_variables';
+@import '_mixins';
+
+.video-channel {
+  @include row-blocks;
+
+  img {
+    @include avatar(80px);
+
+    margin-right: 10px;
+  }
+
+  .video-channel-info {
+    flex-grow: 1;
+
+    a.video-channel-names {
+      @include disable-default-a-behaviour;
+
+      width: fit-content;
+      display: flex;
+      align-items: baseline;
+      color: #000;
+
+      .video-channel-display-name {
+        font-weight: $font-semibold;
+        font-size: 18px;
+      }
+
+      .video-channel-name {
+        font-size: 14px;
+        color: $grey-actor-name;
+        margin-left: 5px;
+      }
+    }
+  }
+
+  .actor-owner {
+    @include actor-owner;
+  }
+
+  my-subscribe-button {
+    /deep/ span[role=button] {
+      padding: 7px 12px;
+      font-size: 16px;
+    }
+  }
+}
+
+
diff --git a/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts b/client/src/app/+my-account/my-account-subscriptions/my-account-subscriptions.component.ts
new file mode 100644 (file)
index 0000000..1e94cf9
--- /dev/null
@@ -0,0 +1,30 @@
+import { Component, OnInit } from '@angular/core'
+import { NotificationsService } from 'angular2-notifications'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { UserSubscriptionService } from '@app/shared/user-subscription'
+
+@Component({
+  selector: 'my-account-subscriptions',
+  templateUrl: './my-account-subscriptions.component.html',
+  styleUrls: [ './my-account-subscriptions.component.scss' ]
+})
+export class MyAccountSubscriptionsComponent implements OnInit {
+  videoChannels: VideoChannel[] = []
+
+  constructor (
+    private userSubscriptionService: UserSubscriptionService,
+    private notificationsService: NotificationsService,
+    private i18n: I18n
+  ) {}
+
+  ngOnInit () {
+    this.userSubscriptionService.listSubscriptions()
+      .subscribe(
+        res => { console.log(res); this.videoChannels = res.data },
+
+        error => this.notificationsService.error(this.i18n('Error'), error.message)
+      )
+  }
+
+}
index e25037e242446b0d59e2bb4e75a8974256bfa3de..56697030be31cfd6e79b112c377e20a7e648c7e0 100644 (file)
@@ -78,7 +78,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
       support: body.support || null
     }
 
-    this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.uuid, videoChannelUpdate).subscribe(
+    this.videoChannelService.updateVideoChannel(this.videoChannelToUpdate.name, videoChannelUpdate).subscribe(
       () => {
         this.authService.refreshUserInformation()
         this.notificationsService.success(
@@ -93,7 +93,7 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
   }
 
   onAvatarChange (formData: FormData) {
-    this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.uuid, formData)
+    this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.name, formData)
         .subscribe(
           data => {
             this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
index f8fd2684e7b74e80b10f4ab47ef663286f53eac7..5c892be012b2089fed84aafcbfcaf2c7a786ae38 100644 (file)
 }
 
 .video-channel {
-  display: flex;
-  min-height: 130px;
-  padding-bottom: 20px;
-  margin-bottom: 20px;
-  border-bottom: 1px solid #C6C6C6;
+  @include row-blocks;
 
   img {
     @include avatar(80px);
@@ -42,7 +38,7 @@
 
       .video-channel-name {
         font-size: 14px;
-        color: #777272;
+        color: $grey-actor-name;
         margin-left: 5px;
       }
     }
   }
 
   .video-channel {
-    flex-direction: column;
-    height: auto;
-    text-align: center;
-
     .video-channel-names {
-      justify-content: center;
+      flex-direction: column;
+      align-items: center !important;
     }
 
     img {
index 64a04fa20a1273f33f48ed6c627e4c8db994ab92..cd805be73b3b1320e9da44ee6e7341cdafbce914 100644 (file)
 }
 
 .video {
-  display: flex;
-  min-height: 130px;
-  padding-bottom: 20px;
-  margin-bottom: 20px;
-  border-bottom: 1px solid #C6C6C6;
+  @include row-blocks;
 
   &:first-child {
     margin-top: 47px;
index ddb0570dbf93dd4f94b06272fc918b5245f5f208..74742649c28afdfca0064c5f1e1e7ee00117cba0 100644 (file)
@@ -2,11 +2,13 @@
   <div class="sub-menu">
     <a i18n routerLink="/my-account/settings" routerLinkActive="active" class="title-page">My settings</a>
 
-    <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My video channels</a>
+    <a i18n routerLink="/my-account/video-channels" routerLinkActive="active" class="title-page">My channels</a>
 
     <a i18n routerLink="/my-account/videos" routerLinkActive="active" class="title-page">My videos</a>
 
-    <a *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My video imports</a>
+    <a i18n routerLink="/my-account/subscriptions" routerLinkActive="active" class="title-page">My subscriptions</a>
+
+    <a *ngIf="isVideoImportEnabled()" i18n routerLink="/my-account/video-imports" routerLinkActive="active" class="title-page">My imports</a>
   </div>
 
   <div class="margin-content">
index 29b49e8d965d2cab8efe44d7b225c91aa92bff31..c93f38d4b135f310c2e06595046cdd59a4a1702f 100644 (file)
@@ -14,6 +14,7 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
 import { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
 import { MyAccountVideoImportsComponent } from '@app/+my-account/my-account-video-imports/my-account-video-imports.component'
 import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settings/my-account-danger-zone'
+import { MyAccountSubscriptionsComponent } from '@app/+my-account/my-account-subscriptions/my-account-subscriptions.component'
 
 @NgModule({
   imports: [
@@ -34,7 +35,8 @@ import { MyAccountDangerZoneComponent } from '@app/+my-account/my-account-settin
     MyAccountVideoChannelUpdateComponent,
     ActorAvatarInfoComponent,
     MyAccountVideoImportsComponent,
-    MyAccountDangerZoneComponent
+    MyAccountDangerZoneComponent,
+    MyAccountSubscriptionsComponent
   ],
 
   exports: [
index 36a792f82b8ded7995212910b53cc2c39b66f6b2..0b0c83de55f808aa4585cae9974ed6ae3890b5a2 100644 (file)
@@ -25,7 +25,7 @@
         position: relative;
         top: 2px;
         font-size: 14px;
-        color: #777272;
+        color: $grey-actor-name;
       }
     }
 
index 5a69a82a04ee69c7fcee535dc01e099172ddb58b..1941a2eab436cc0274605634f16e0d1361f2dad7 100644 (file)
@@ -8,6 +8,8 @@
         <div class="actor-names">
           <div class="actor-display-name">{{ videoChannel.displayName }}</div>
           <div class="actor-name">{{ videoChannel.nameWithHost }}</div>
+
+          <my-subscribe-button [videoChannel]="videoChannel"></my-subscribe-button>
         </div>
         <div i18n class="actor-followers">{{ videoChannel.followersCount }} subscribers</div>
 
@@ -20,7 +22,6 @@
 
     <div class="links">
       <a i18n routerLink="videos" routerLinkActive="active" class="title-page">Videos</a>
-
       <a i18n routerLink="about" routerLinkActive="active" class="title-page">About</a>
     </div>
   </div>
index 909b65bc7118d71373c079278d9d6ddc5393a915..a63b1ec06f5ac74e08d8d68f8d10c2d1cedea1cb 100644 (file)
@@ -3,4 +3,19 @@
 
 .sub-menu {
   @include sub-menu-with-actor;
+
+  .actor, .actor-info {
+    width: 100%;
+  }
+
+  .actor-name {
+    flex-grow: 1;
+  }
+
+  my-subscribe-button {
+    /deep/ span[role=button] {
+      padding: 7px 12px;
+      font-size: 16px;
+    }
+  }
 }
\ No newline at end of file
index 7edcdf501ade38952138d22544e27ed904889f85..bd03af9b3c940f497ac095c60eb23e75351d322d 100644 (file)
       <div class="panel-block">
         <div i18n class="block-title">Videos</div>
 
+        <a *ngIf="isLoggedIn" routerLink="/videos/subscriptions" routerLinkActive="active">
+          <span class="icon icon-videos-subscriptions"></span>
+          <ng-container i18n>Subscriptions</ng-container>
+        </a>
+
         <a routerLink="/videos/trending" routerLinkActive="active">
           <span class="icon icon-videos-trending"></span>
           <ng-container i18n>Trending</ng-container>
index 39f1e9be0f2d77cd1864c95d83f08a730f09d339..606fea96154785bc1fbe103aca795f867c2f94f1 100644 (file)
@@ -135,6 +135,12 @@ menu {
 
         margin-right: 18px;
 
+        &.icon-videos-subscriptions {
+          position: relative;
+          top: -2px;
+          background-image: url('../../assets/images/menu/subscriptions.svg');
+        }
+
         &.icon-videos-trending {
           position: relative;
           top: -2px;
index 722415a06bf3e22011b0bcbca487eaea8af64d70..9bc7ad88b479aa9fc7559410c18fa0049bc33cac 100644 (file)
@@ -36,7 +36,8 @@ import {
   ReactiveFileComponent,
   ResetPasswordValidatorsService,
   UserValidatorsService,
-  VideoAbuseValidatorsService, VideoBlacklistValidatorsService,
+  VideoAbuseValidatorsService,
+  VideoBlacklistValidatorsService,
   VideoChannelValidatorsService,
   VideoCommentValidatorsService,
   VideoValidatorsService
@@ -49,6 +50,7 @@ import { PeertubeCheckboxComponent } from '@app/shared/forms/peertube-checkbox.c
 import { VideoImportService } from '@app/shared/video-import/video-import.service'
 import { ActionDropdownComponent } from '@app/shared/buttons/action-dropdown.component'
 import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
+import { SubscribeButtonComponent, UserSubscriptionService } from '@app/shared/user-subscription'
 
 @NgModule({
   imports: [
@@ -83,7 +85,8 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
     InfiniteScrollerDirective,
     HelpComponent,
     ReactiveFileComponent,
-    PeertubeCheckboxComponent
+    PeertubeCheckboxComponent,
+    SubscribeButtonComponent
   ],
 
   exports: [
@@ -115,6 +118,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
     HelpComponent,
     ReactiveFileComponent,
     PeertubeCheckboxComponent,
+    SubscribeButtonComponent,
 
     NumberFormatterPipe,
     ObjectLengthPipe,
@@ -134,6 +138,7 @@ import { NgbDropdownModule, NgbModalModule, NgbPopoverModule, NgbTabsetModule, N
     VideoChannelService,
     VideoCaptionService,
     VideoImportService,
+    UserSubscriptionService,
 
     FormValidatorService,
     CustomConfigValidatorsService,
diff --git a/client/src/app/shared/user-subscription/index.ts b/client/src/app/shared/user-subscription/index.ts
new file mode 100644 (file)
index 0000000..024b36a
--- /dev/null
@@ -0,0 +1,2 @@
+export * from './user-subscription.service'
+export * from './subscribe-button.component'
\ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.html b/client/src/app/shared/user-subscription/subscribe-button.component.html
new file mode 100644 (file)
index 0000000..63b3136
--- /dev/null
@@ -0,0 +1,15 @@
+<span i18n *ngIf="subscribed === false" class="subscribe-button" role="button" (click)="subscribe()">
+  <span>Subscribe</span>
+  <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+    {{ videoChannel.followersCount | myNumberFormatter }}
+  </span>
+</span>
+
+<span *ngIf="subscribed === true" class="unsubscribe-button" role="button" (click)="unsubscribe()">
+  <span class="subscribed" i18n>Subscribed</span>
+  <span class="unsubscribe" i18n>Unsubscribe</span>
+
+  <span *ngIf="displayFollowers && videoChannel.followersCount !== 0" class="followers-count">
+    {{ videoChannel.followersCount | myNumberFormatter }}
+  </span>
+</span>
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.scss b/client/src/app/shared/user-subscription/subscribe-button.component.scss
new file mode 100644 (file)
index 0000000..9811fdc
--- /dev/null
@@ -0,0 +1,37 @@
+@import '_variables';
+@import '_mixins';
+
+.subscribe-button {
+  @include peertube-button;
+  @include orange-button;
+}
+
+.unsubscribe-button {
+  @include peertube-button;
+  @include grey-button
+}
+
+.subscribe-button,
+.unsubscribe-button {
+  padding: 3px 7px;
+}
+
+.unsubscribe-button {
+  .subscribed {
+    display: inline;
+  }
+
+  .unsubscribe {
+    display: none;
+  }
+
+  &:hover {
+    .subscribed {
+      display: none;
+    }
+
+    .unsubscribe {
+      display: inline;
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/app/shared/user-subscription/subscribe-button.component.ts b/client/src/app/shared/user-subscription/subscribe-button.component.ts
new file mode 100644 (file)
index 0000000..46d6dba
--- /dev/null
@@ -0,0 +1,74 @@
+import { Component, Input, OnInit } from '@angular/core'
+import { AuthService } from '@app/core'
+import { RestExtractor } from '@app/shared/rest'
+import { RedirectService } from '@app/core/routing/redirect.service'
+import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { NotificationsService } from 'angular2-notifications'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+
+@Component({
+  selector: 'my-subscribe-button',
+  templateUrl: './subscribe-button.component.html',
+  styleUrls: [ './subscribe-button.component.scss' ]
+})
+export class SubscribeButtonComponent implements OnInit {
+  @Input() videoChannel: VideoChannel
+  @Input() displayFollowers = false
+
+  subscribed: boolean
+
+  constructor (
+    private authService: AuthService,
+    private restExtractor: RestExtractor,
+    private redirectService: RedirectService,
+    private notificationsService: NotificationsService,
+    private userSubscriptionService: UserSubscriptionService,
+    private i18n: I18n
+  ) { }
+
+  get uri () {
+    return this.videoChannel.name + '@' + this.videoChannel.host
+  }
+
+  ngOnInit () {
+    this.userSubscriptionService.isSubscriptionExists(this.uri)
+      .subscribe(
+        exists => this.subscribed = exists,
+
+        err => this.notificationsService.error(this.i18n('Error'), err.message)
+      )
+  }
+
+  subscribe () {
+    this.userSubscriptionService.addSubscription(this.uri)
+      .subscribe(
+        () => {
+          this.subscribed = true
+
+          this.notificationsService.success(
+            this.i18n('Subscribed'),
+            this.i18n('Subscribed to {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName })
+          )
+        },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+      )
+  }
+
+  unsubscribe () {
+    this.userSubscriptionService.deleteSubscription(this.uri)
+        .subscribe(
+          () => {
+            this.subscribed = false
+
+            this.notificationsService.success(
+              this.i18n('Unsubscribed'),
+              this.i18n('Unsubscribed from {{nameWithHost}}', { nameWithHost: this.videoChannel.displayName })
+            )
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+}
diff --git a/client/src/app/shared/user-subscription/user-subscription.service.ts b/client/src/app/shared/user-subscription/user-subscription.service.ts
new file mode 100644 (file)
index 0000000..3103706
--- /dev/null
@@ -0,0 +1,66 @@
+import { catchError, map } from 'rxjs/operators'
+import { HttpClient } from '@angular/common/http'
+import { Injectable } from '@angular/core'
+import { ResultList } from '../../../../../shared'
+import { environment } from '../../../environments/environment'
+import { RestExtractor } from '../rest'
+import { Observable, of } from 'rxjs'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
+import { VideoChannel as VideoChannelServer } from '../../../../../shared/models/videos'
+
+@Injectable()
+export class UserSubscriptionService {
+  static BASE_USER_SUBSCRIPTIONS_URL = environment.apiUrl + '/api/v1/users/me/subscriptions'
+
+  constructor (
+    private authHttp: HttpClient,
+    private restExtractor: RestExtractor
+  ) {
+  }
+
+  deleteSubscription (nameWithHost: string) {
+    const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost
+
+    return this.authHttp.delete(url)
+               .pipe(
+                 map(this.restExtractor.extractDataBool),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
+  addSubscription (nameWithHost: string) {
+    const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
+
+    const body = { uri: nameWithHost }
+    return this.authHttp.post(url, body)
+               .pipe(
+                 map(this.restExtractor.extractDataBool),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
+  listSubscriptions (): Observable<ResultList<VideoChannel>> {
+    const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL
+
+    return this.authHttp.get<ResultList<VideoChannelServer>>(url)
+               .pipe(
+                 map(res => VideoChannelService.extractVideoChannels(res)),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
+  isSubscriptionExists (nameWithHost: string): Observable<boolean> {
+    const url = UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/' + nameWithHost
+
+    return this.authHttp.get(url)
+               .pipe(
+                 map(this.restExtractor.extractDataBool),
+                 catchError(err => {
+                   if (err.status === 404) return of(false)
+
+                   return this.restExtractor.handleError(err)
+                 })
+               )
+  }
+}
index 510dc9c3d31a5dd08c422dd5584c5f5448e6eb65..46b1217904d45ef3f7a89786f9f9891f4f180fa0 100644 (file)
@@ -22,6 +22,16 @@ export class VideoChannelService {
     private restExtractor: RestExtractor
   ) {}
 
+  static extractVideoChannels (result: ResultList<VideoChannelServer>) {
+    const videoChannels: VideoChannel[] = []
+
+    for (const videoChannelJSON of result.data) {
+      videoChannels.push(new VideoChannel(videoChannelJSON))
+    }
+
+    return { data: videoChannels, total: result.total }
+  }
+
   getVideoChannel (videoChannelName: string) {
     return this.authHttp.get<VideoChannel>(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName)
                .pipe(
@@ -34,7 +44,7 @@ export class VideoChannelService {
   listAccountVideoChannels (account: Account): Observable<ResultList<VideoChannel>> {
     return this.authHttp.get<ResultList<VideoChannelServer>>(AccountService.BASE_ACCOUNT_URL + account.nameWithHost + '/video-channels')
                .pipe(
-                 map(res => this.extractVideoChannels(res)),
+                 map(res => VideoChannelService.extractVideoChannels(res)),
                  catchError(err => this.restExtractor.handleError(err))
                )
   }
@@ -47,16 +57,16 @@ export class VideoChannelService {
                )
   }
 
-  updateVideoChannel (videoChannelUUID: string, videoChannel: VideoChannelUpdate) {
-    return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID, videoChannel)
+  updateVideoChannel (videoChannelName: string, videoChannel: VideoChannelUpdate) {
+    return this.authHttp.put(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName, videoChannel)
                .pipe(
                  map(this.restExtractor.extractDataBool),
                  catchError(err => this.restExtractor.handleError(err))
                )
   }
 
-  changeVideoChannelAvatar (videoChannelUUID: string, avatarForm: FormData) {
-    const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID + '/avatar/pick'
+  changeVideoChannelAvatar (videoChannelName: string, avatarForm: FormData) {
+    const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar/pick'
 
     return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
                .pipe(catchError(err => this.restExtractor.handleError(err)))
@@ -69,14 +79,4 @@ export class VideoChannelService {
                  catchError(err => this.restExtractor.handleError(err))
                )
   }
-
-  private extractVideoChannels (result: ResultList<VideoChannelServer>) {
-    const videoChannels: VideoChannel[] = []
-
-    for (const videoChannelJSON of result.data) {
-      videoChannels.push(new VideoChannel(videoChannelJSON))
-    }
-
-    return { data: videoChannels, total: result.total }
-  }
 }
index e8ded6ab8c63d53233952df8fba4adc853d815c2..d4b00c07c1e743fabc166e96d51a3daa8841ee5b 100644 (file)
@@ -14,7 +14,7 @@
     <div *ngFor="let videos of videoPages" class="videos-page">
       <my-video-miniature
         class="ng-animate"
-        *ngFor="let video of videos" [video]="video" [user]="user"
+        *ngFor="let video of videos" [video]="video" [user]="user" [ownerDisplayType]="ownerDisplayType"
       >
       </my-video-miniature>
     </div>
index 59d3c1ebee18a9dc1c1cc87a981d2ba2b0acdd3b..b8fd7f8eb21b85b289881c48bda96eb891b30aab 100644 (file)
@@ -11,6 +11,7 @@ import { VideoSortField } from './sort-field.type'
 import { Video } from './video.model'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { ScreenService } from '@app/shared/misc/screen.service'
+import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
 
 export abstract class AbstractVideoList implements OnInit, OnDestroy {
   private static LINES_PER_PAGE = 4
@@ -34,6 +35,7 @@ export abstract class AbstractVideoList implements OnInit, OnDestroy {
   videoWidth: number
   videoHeight: number
   videoPages: Video[][] = []
+  ownerDisplayType: OwnerDisplayType = 'account'
 
   protected baseVideoWidth = 215
   protected baseVideoHeight = 230
index d346f985cbe2be5fa712e5c8912b2f5d0658fea2..fa4ca7f9391014a7729dbcbfe6c05a2fc902ab58 100644 (file)
@@ -1,14 +1,8 @@
-import {
-  UserRight,
-  VideoChannel,
-  VideoConstant,
-  VideoDetails as VideoDetailsServerModel,
-  VideoFile,
-  VideoState
-} from '../../../../../shared'
+import { UserRight, VideoConstant, VideoDetails as VideoDetailsServerModel, VideoFile, VideoState } from '../../../../../shared'
 import { AuthUser } from '../../core'
 import { Video } from '../../shared/video/video.model'
 import { Account } from '@app/shared/account/account.model'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
 
 export class VideoDetails extends Video implements VideoDetailsServerModel {
   descriptionPath: string
@@ -30,7 +24,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
 
     this.descriptionPath = hash.descriptionPath
     this.files = hash.files
-    this.channel = hash.channel
+    this.channel = new VideoChannel(hash.channel)
     this.account = new Account(hash.account)
     this.tags = hash.tags
     this.support = hash.support
index 3010e5ccc2864047c1122d56e3c1f2bd4788ca44..de84bccf96a216015296e5095f8c4f5f7f7cc94e 100644 (file)
     </a>
 
     <span i18n class="video-miniature-created-at-views">{{ video.publishedAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
-    <a class="video-miniature-account" [routerLink]="[ '/accounts', video.by ]">{{ video.by }}</a>
+
+    <a *ngIf="displayOwnerAccount()" class="video-miniature-account" [routerLink]="[ '/accounts', video.byAccount ]">
+      {{ video.byAccount }}
+    </a>
+    <a *ngIf="displayOwnerVideoChannel()" class="video-miniature-channel" [routerLink]="[ '/video-channels', video.byVideoChannel ]">
+      {{ video.byVideoChannel }}
+    </a>
   </div>
 </div>
index 588eea3a757081114acaa6f84ea3dbdf88fc2156..6883650f4e55f643765f0a260991a180c40e570a 100644 (file)
@@ -38,7 +38,8 @@
       font-size: 13px;
     }
 
-    .video-miniature-account {
+    .video-miniature-account,
+    .video-miniature-channel {
       @include disable-default-a-behaviour;
 
       display: block;
index d3f6dc1f6dae9c8b32567f888bc16a30faa20883..07193ebd580d1cd5f02bd391a7c870faf1d9de4b 100644 (file)
@@ -1,20 +1,51 @@
-import { Component, Input } from '@angular/core'
+import { Component, Input, OnInit } from '@angular/core'
 import { User } from '../users'
 import { Video } from './video.model'
 import { ServerService } from '@app/core'
 
+export type OwnerDisplayType = 'account' | 'videoChannel' | 'auto'
+
 @Component({
   selector: 'my-video-miniature',
   styleUrls: [ './video-miniature.component.scss' ],
   templateUrl: './video-miniature.component.html'
 })
-export class VideoMiniatureComponent {
+export class VideoMiniatureComponent implements OnInit {
   @Input() user: User
   @Input() video: Video
+  @Input() ownerDisplayType: OwnerDisplayType = 'account'
+
+  private ownerDisplayTypeChosen: 'account' | 'videoChannel'
 
   constructor (private serverService: ServerService) { }
 
+  ngOnInit () {
+    if (this.ownerDisplayType === 'account' || this.ownerDisplayType === 'videoChannel') {
+      this.ownerDisplayTypeChosen = this.ownerDisplayType
+      return
+    }
+
+    // If the video channel name an UUID (not really displayable, we changed this behaviour in v1.0.0-beta.12)
+    // -> Use the account name
+    if (
+      this.video.channel.name === `${this.video.account.name}_channel` ||
+      this.video.channel.name.match(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
+    ) {
+      this.ownerDisplayTypeChosen = 'account'
+    } else {
+      this.ownerDisplayTypeChosen = 'videoChannel'
+    }
+  }
+
   isVideoBlur () {
     return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
   }
+
+  displayOwnerAccount () {
+    return this.ownerDisplayTypeChosen === 'account'
+  }
+
+  displayOwnerVideoChannel () {
+    return this.ownerDisplayTypeChosen === 'videoChannel'
+  }
 }
index df825330173ed45e92c6fbb998d16ca4b5bf0283..d80c104590b97e80ddae8269a649a76b92cddb60 100644 (file)
@@ -8,9 +8,12 @@ import { Actor } from '@app/shared/actor/actor.model'
 import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-schedule-update.model'
 
 export class Video implements VideoServerModel {
-  by: string
+  byVideoChannel: string
+  byAccount: string
+
   accountAvatarUrl: string
   videoChannelAvatarUrl: string
+
   createdAt: Date
   updatedAt: Date
   publishedAt: Date
@@ -110,7 +113,8 @@ export class Video implements VideoServerModel {
     this.account = hash.account
     this.channel = hash.channel
 
-    this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
+    this.byAccount = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
+    this.byVideoChannel = Actor.CREATE_BY_STRING(hash.channel.name, hash.channel.host)
     this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
     this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel)
 
index e44f1ee65a2e7de096608b3f6c0f1405040bed8c..1a934c8e2dd0c48de20b77b13b5f7295d9e5667b 100644 (file)
@@ -27,6 +27,7 @@ import { Account } from '@app/shared/account/account.model'
 import { AccountService } from '@app/shared/account/account.service'
 import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
 import { ServerService } from '@app/core'
+import { UserSubscriptionService } from '@app/shared/user-subscription'
 
 @Injectable()
 export class VideoService {
@@ -157,6 +158,23 @@ export class VideoService {
                )
   }
 
+  getUserSubscriptionVideos (
+    videoPagination: ComponentPagination,
+    sort: VideoSortField
+  ): Observable<{ videos: Video[], totalVideos: number }> {
+    const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
+
+    let params = new HttpParams()
+    params = this.restService.addRestGetParams(params, pagination, sort)
+
+    return this.authHttp
+               .get<ResultList<Video>>(UserSubscriptionService.BASE_USER_SUBSCRIPTIONS_URL + '/videos', { params })
+               .pipe(
+                 switchMap(res => this.extractVideos(res)),
+                 catchError(err => this.restExtractor.handleError(err))
+               )
+  }
+
   getVideos (
     videoPagination: ComponentPagination,
     sort: VideoSortField,
index c275258effc1b04785125a19b1e3c0b71e487285..8a49e3566f9eb8396fc2ab966d56a8e01d4eef5f 100644 (file)
 
               <img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
             </a>
-            <!-- Here will be the subscribe button -->
+
+            <my-subscribe-button [videoChannel]="video.channel"></my-subscribe-button>
           </div>
 
           <div class="video-info-by">
-            <a [routerLink]="[ '/accounts', video.by ]" i18n-title title="Go to the account page">
-              <span i18n>By {{ video.by }}</span>
+            <a [routerLink]="[ '/accounts', video.byAccount ]" i18n-title title="Go to the account page">
+              <span i18n>By {{ video.byAccount }}</span>
               <img [src]="video.accountAvatarUrl" alt="Account avatar" />
             </a>
 
-            <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there. Subscription as a PeerTube user is being worked on in <a href='https://github.com/Chocobozzz/PeerTube/issues/470'>#470</a>."></my-help>
+            <my-help helpType="custom" i18n-customHtml customHtml="You can subscribe to this account via any ActivityPub-capable fediverse instance. For instance with Mastodon or Pleroma you can type in the search box <strong>@{{video.account.name}}@{{video.account.host}}</strong> and subscribe there."></my-help>
           </div>
         </div>
 
index 1354de32e0f24bcf0750ff64a451b59dfc921985..5bf2f485aa261d44590ec28f277cfb341cac8f93 100644 (file)
             margin: -2px 2px 0 5px;
           }
         }
+
+        my-subscribe-button {
+          /deep/ span[role=button] {
+            font-size: 13px !important;
+          }
+
+          margin-left: 5px;
+        }
       }
 
       .video-info-by {
 
       .video-miniature-information {
         flex-grow: 1;
-        margin-left: 10px;
+      }
+
+      .video-thumbnail {
+        margin-right: 10px
       }
     }
   }
   .other-videos {
     /deep/ .video-miniature  {
       flex-direction: column;
-
-      .video-miniature-information {
-        margin-left: 0 !important;
-      }
     }
   }
 
diff --git a/client/src/app/videos/video-list/video-user-subscriptions.component.ts b/client/src/app/videos/video-list/video-user-subscriptions.component.ts
new file mode 100644 (file)
index 0000000..6e8959c
--- /dev/null
@@ -0,0 +1,57 @@
+import { Component, OnDestroy, OnInit } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
+import { immutableAssign } from '@app/shared/misc/utils'
+import { Location } from '@angular/common'
+import { NotificationsService } from 'angular2-notifications'
+import { AuthService } from '../../core/auth'
+import { AbstractVideoList } from '../../shared/video/abstract-video-list'
+import { VideoSortField } from '../../shared/video/sort-field.type'
+import { VideoService } from '../../shared/video/video.service'
+import { I18n } from '@ngx-translate/i18n-polyfill'
+import { ScreenService } from '@app/shared/misc/screen.service'
+import { OwnerDisplayType } from '@app/shared/video/video-miniature.component'
+
+@Component({
+  selector: 'my-videos-user-subscriptions',
+  styleUrls: [ '../../shared/video/abstract-video-list.scss' ],
+  templateUrl: '../../shared/video/abstract-video-list.html'
+})
+export class VideoUserSubscriptionsComponent extends AbstractVideoList implements OnInit, OnDestroy {
+  titlePage: string
+  currentRoute = '/videos/subscriptions'
+  sort = '-publishedAt' as VideoSortField
+  ownerDisplayType: OwnerDisplayType = 'auto'
+
+  constructor (
+    protected router: Router,
+    protected route: ActivatedRoute,
+    protected notificationsService: NotificationsService,
+    protected authService: AuthService,
+    protected location: Location,
+    protected i18n: I18n,
+    protected screenService: ScreenService,
+    private videoService: VideoService
+  ) {
+    super()
+
+    this.titlePage = i18n('Videos from your subscriptions')
+  }
+
+  ngOnInit () {
+    super.ngOnInit()
+  }
+
+  ngOnDestroy () {
+    super.ngOnDestroy()
+  }
+
+  getVideosObservable (page: number) {
+    const newPagination = immutableAssign(this.pagination, { currentPage: page })
+
+    return this.videoService.getUserSubscriptionVideos(newPagination, this.sort)
+  }
+
+  generateSyndicationList () {
+    // not implemented yet
+  }
+}
index 538a43c6dc3c486548dd5dd4947d8fa8efaa58ae..18ed5257064e00781860929746fdb2916d7e8056 100644 (file)
@@ -5,6 +5,7 @@ import { MetaGuard } from '@ngx-meta/core'
 import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.component'
 import { VideoTrendingComponent } from './video-list/video-trending.component'
 import { VideosComponent } from './videos.component'
+import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component'
 
 const videosRoutes: Routes = [
   {
@@ -12,11 +13,6 @@ const videosRoutes: Routes = [
     component: VideosComponent,
     canActivateChild: [ MetaGuard ],
     children: [
-      {
-        path: 'list',
-        pathMatch: 'full',
-        redirectTo: 'recently-added'
-      },
       {
         path: 'trending',
         component: VideoTrendingComponent,
@@ -35,6 +31,15 @@ const videosRoutes: Routes = [
           }
         }
       },
+      {
+        path: 'subscriptions',
+        component: VideoUserSubscriptionsComponent,
+        data: {
+          meta: {
+            title: 'Subscriptions'
+          }
+        }
+      },
       {
         path: 'local',
         component: VideoLocalComponent,
index c38257e0889d4d4e570cb7a1bb8322ae14dd2d34..3c38772734139f80a6eb2c5dca24f23ab90359bc 100644 (file)
@@ -5,6 +5,7 @@ import { VideoRecentlyAddedComponent } from './video-list/video-recently-added.c
 import { VideoTrendingComponent } from './video-list/video-trending.component'
 import { VideosRoutingModule } from './videos-routing.module'
 import { VideosComponent } from './videos.component'
+import { VideoUserSubscriptionsComponent } from '@app/videos/video-list/video-user-subscriptions.component'
 
 @NgModule({
   imports: [
@@ -17,7 +18,8 @@ import { VideosComponent } from './videos.component'
 
     VideoTrendingComponent,
     VideoRecentlyAddedComponent,
-    VideoLocalComponent
+    VideoLocalComponent,
+    VideoUserSubscriptionsComponent
   ],
 
   exports: [
diff --git a/client/src/assets/images/menu/podcasts.svg b/client/src/assets/images/menu/podcasts.svg
new file mode 100644 (file)
index 0000000..cd6efc5
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
+    <title>podcasts</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1">
+            <stop stop-color="#808080" offset="0%"></stop>
+            <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2">
+            <stop stop-color="#808080" offset="0%"></stop>
+            <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Artboard-4" transform="translate(-532.000000, -775.000000)">
+            <g id="312" transform="translate(532.000000, 775.000000)">
+                <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle>
+                <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
+                <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
+                <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path>
+            </g>
+        </g>
+    </g>
+</svg>
diff --git a/client/src/assets/images/menu/subscriptions.svg b/client/src/assets/images/menu/subscriptions.svg
new file mode 100644 (file)
index 0000000..cd6efc5
--- /dev/null
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 43.2 (39069) - http://www.bohemiancoding.com/sketch -->
+    <title>podcasts</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="97.3333865%" id="linearGradient-1">
+            <stop stop-color="#808080" offset="0%"></stop>
+            <stop stop-color="#808080" stop-opacity="0.247310915" offset="100%"></stop>
+        </linearGradient>
+        <linearGradient x1="50%" y1="0%" x2="50%" y2="97.8635204%" id="linearGradient-2">
+            <stop stop-color="#808080" offset="0%"></stop>
+            <stop stop-color="#808080" stop-opacity="0.250707654" offset="100%"></stop>
+        </linearGradient>
+    </defs>
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Artboard-4" transform="translate(-532.000000, -775.000000)">
+            <g id="312" transform="translate(532.000000, 775.000000)">
+                <circle id="Oval-169" fill="#808080" cx="12" cy="10" r="3"></circle>
+                <path d="M16.3851456,13.8501206 C17.4222377,12.6991612 18,11.4167199 18,10 C18,6.74089158 15.2591084,4 12,4 C8.74089158,4 6,6.74089158 6,10 C6,11.4186069 6.57916224,12.7027674 7.61838071,13.8540306 C7.80341316,14.0590125 8.11958231,14.0751848 8.32456427,13.8901523 C8.52954623,13.7051199 8.5457185,13.3889507 8.36068606,13.1839688 C7.47616718,12.2040844 7,11.148292 7,10 C7,7.29317633 9.29317633,5 12,5 C14.7068237,5 17,7.29317633 17,10 C17,11.1466944 16.5249958,12.2010466 15.6422459,13.1807178 C15.4573954,13.3858639 15.4738483,13.7020185 15.6789944,13.886869 C15.8841405,14.0717195 16.2002951,14.0552666 16.3851456,13.8501206 Z" id="Oval-169" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
+                <path d="M17.5678226,18.3077078 C20.3159646,16.4626239 22,13.3733223 22,10 C22,4.4771525 17.5228475,0 12,0 C6.4771525,0 2,4.4771525 2,10 C2,13.3762414 3.68696556,16.4678678 6.43901638,18.3122954 C6.89779529,18.6197696 7.51896613,18.4971129 7.82644029,18.0383339 C8.13391444,17.579555 8.0112577,16.9583842 7.55247879,16.65091 C5.34877306,15.1739839 4,12.7021478 4,10 C4,5.581722 7.581722,2 12,2 C16.418278,2 20,5.581722 20,10 C20,12.699815 18.6535741,15.1697843 16.4529947,16.6472384 C15.9944687,16.9550897 15.8723227,17.5763611 16.180174,18.0348871 C16.4880252,18.4934131 17.1092967,18.6155591 17.5678226,18.3077078 Z" id="Oval-169" fill="url(#linearGradient-2)" fill-rule="nonzero"></path>
+                <path d="M9.32918137,15.9750882 C9.14737952,14.8842771 9.89826062,14 10.9979131,14 L13.0020869,14 C14.1055038,14 14.8534426,14.8793447 14.6708186,15.9750882 L13.6633817,22.0197096 C13.5731485,22.561109 13.0573397,23 12.5010434,23 L11.4989566,23 C10.9472481,23 10.4276519,22.5659113 10.3366183,22.0197096 L9.32918137,15.9750882 Z" id="Rectangle-217" fill="#808080"></path>
+            </g>
+        </g>
+    </g>
+</svg>
index dc0ffe912a67ed6186abb7840d7c43b423ef16bd..b2d7c2bec9bea155248c70ad5b56ba04bdf29684 100644 (file)
@@ -83,6 +83,7 @@ label {
     display: flex;
     align-items: center;
     padding-left: $not-expanded-horizontal-margins;
+    padding-right: $not-expanded-horizontal-margins;
   }
 
   // Override some properties if the main content is expanded (no menu on the left)
@@ -96,6 +97,7 @@ label {
 
     .sub-menu {
       padding-left: $expanded-horizontal-margins;
+      padding-right: $expanded-horizontal-margins;
     }
   }
 }
@@ -294,6 +296,10 @@ table {
 
       .sub-menu {
         padding-left: 50px;
+
+        .title-page {
+          font-size: 15px;
+        }
       }
     }
   }
@@ -316,6 +322,7 @@ table {
 
       .sub-menu {
         padding-left: 15px;
+        padding-right: 15px;
         margin-bottom: 10px;
       }
 
index b0b0f544c2f8e939e12351407c438aecf93f9fa3..aafe478f9596007263c1bd2207ca3a5ae756899c 100644 (file)
   font-size: 13px;
 }
 
+@mixin actor-owner {
+  @include disable-default-a-behaviour;
+
+  display: block;
+  font-size: 13px;
+  margin-top: 4px;
+  color: #000;
+
+  span:hover {
+    opacity: 0.8;
+  }
+
+  img {
+    @include avatar(18px);
+
+    margin-left: 7px;
+    position: relative;
+    top: -2px;
+  }
+}
+
 @mixin sub-menu-with-actor {
   height: 160px;
   display: flex;
           position: relative;
           top: 3px;
           font-size: 14px;
-          color: #777272;
+          color: $grey-actor-name;
         }
       }
 
       }
 
       .actor-owner {
-        @include disable-default-a-behaviour;
-
-        display: block;
-        font-size: 13px;
-        margin-top: 4px;
-        color: #000;
-
-        span:hover {
-          opacity: 0.8;
-        }
-
-        img {
-          @include avatar(18px);
-
-          margin-left: 7px;
-          position: relative;
-          top: -2px;
-        }
+        @include actor-owner;
       }
     }
   }
     background-image: url($imageUrl);
   }
 }
+
+@mixin row-blocks {
+  display: flex;
+  min-height: 130px;
+  padding-bottom: 20px;
+  margin-bottom: 20px;
+  border-bottom: 1px solid #C6C6C6;
+
+  @media screen and (max-width: 800px) {
+    flex-direction: column;
+    height: auto;
+    text-align: center;
+    align-items: center;
+  }
+}
\ No newline at end of file
index f1f7551264508849e215ce9e62418df5a97c7cb7..e6db9864276b70f83f022e5ff050869b3ab3929f 100644 (file)
@@ -12,6 +12,8 @@ $black-background: #000;
 $grey-background: #f6f2f2;
 $red-error: #FF0000;
 
+$grey-actor-name: #777272;
+
 $expanded-horizontal-margins: 150px;
 $not-expanded-horizontal-margins: 30px;
 
index 282dde2683eb971b4f56a794dfe2cf6277549996..36d0f237bd03dd6d54cde757feefdb9ceb182c1b 100644 (file)
@@ -1,7 +1,6 @@
 import * as Bull from 'bull'
 import { logger } from '../../../helpers/logger'
-import { getServerActor } from '../../../helpers/utils'
-import { REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers'
+import { CONFIG, REMOTE_SCHEME, sequelizeTypescript } from '../../../initializers'
 import { sendFollow } from '../../activitypub/send'
 import { sanitizeHost } from '../../../helpers/core-utils'
 import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
@@ -22,10 +21,14 @@ async function processActivityPubFollow (job: Bull.Job) {
 
   logger.info('Processing ActivityPub follow in job %d.', job.id)
 
-  const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
-
-  const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
-  const targetActor = await getOrCreateActorAndServerAndModel(actorUrl)
+  let targetActor: ActorModel
+  if (!host || host === CONFIG.WEBSERVER.HOST) {
+    targetActor = await ActorModel.loadLocalByName(payload.name)
+  } else {
+    const sanitizedHost = sanitizeHost(host, REMOTE_SCHEME.HTTP)
+    const actorUrl = await loadActorUrlOrGetFromWebfinger(payload.name + '@' + sanitizedHost)
+    targetActor = await getOrCreateActorAndServerAndModel(actorUrl)
+  }
 
   const fromActor = await ActorModel.load(payload.followerActorId)
 
index 20d3aa5fcbaa7364ac344c0503fc4f6dac01ed62..b2d7ace6698934fac893302c18742b61c014825b 100644 (file)
@@ -29,6 +29,7 @@ import { getSort } from '../utils'
 import { ActorModel } from './actor'
 import { VideoChannelModel } from '../video/video-channel'
 import { IIncludeOptions } from '../../../node_modules/sequelize-typescript/lib/interfaces/IIncludeOptions'
+import { AccountModel } from '../account/account'
 
 @Table({
   tableName: 'actorFollow',
@@ -262,7 +263,13 @@ export class ActorFollowModel extends Model<ActorFollowModel> {
           include: [
             {
               model: VideoChannelModel,
-              required: true
+              required: true,
+              include: [
+                {
+                  model: AccountModel,
+                  required: true
+                }
+              ]
             }
           ]
         }