Client: Add ability to update video channel avatar
authorChocobozzz <me@florianbigard.com>
Fri, 29 Jun 2018 12:34:04 +0000 (14:34 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 29 Jun 2018 12:34:04 +0000 (14:34 +0200)
19 files changed:
client/src/app/+my-account/my-account-settings/my-account-settings.component.html
client/src/app/+my-account/my-account-settings/my-account-settings.component.scss
client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.html
client/src/app/+my-account/my-account-video-channels/my-account-video-channel-edit.component.scss
client/src/app/+my-account/my-account-video-channels/my-account-video-channel-update.component.ts
client/src/app/+my-account/my-account.module.ts
client/src/app/+my-account/shared/actor-avatar-info.component.html [new file with mode: 0644]
client/src/app/+my-account/shared/actor-avatar-info.component.scss [new file with mode: 0644]
client/src/app/+my-account/shared/actor-avatar-info.component.ts [new file with mode: 0644]
client/src/app/shared/actor/actor.model.ts
client/src/app/shared/users/user.model.ts
client/src/app/shared/video-channel/video-channel.service.ts
client/src/app/shared/video/video.model.ts
client/src/app/videos/+video-edit/video-add.component.ts
client/src/app/videos/+video-watch/video-watch.component.html
client/src/app/videos/+video-watch/video-watch.component.scss
server/models/video/video.ts
server/tests/api/check-params/video-channels.ts

index f5d593f19f5714ab8d1ae74ae26a5a2bffc99455..ff08cb7771fc33d505671ca477add55042be8b7f 100644 (file)
@@ -1,20 +1,4 @@
-<div class="user">
-  <img [src]="user.accountAvatarUrl" alt="Avatar" />
-
-  <div class="user-info">
-    <div class="user-info-names">
-      <div class="user-info-display-name">{{ user.account?.displayName }}</div>
-      <div class="user-info-username">{{ user.username }}</div>
-    </div>
-    <div i18n class="user-info-followers">{{ user.account?.followersCount }} subscribers</div>
-  </div>
-</div>
-
-<div class="button-file">
-  <span i18n>Change your avatar</span>
-  <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="changeAvatar()" />
-</div>
-<div i18n class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
+<my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)"></my-actor-avatar-info>
 
 <div class="user-quota">
   <span i18n class="user-quota-label">Video quota:</span> {{ userVideoQuotaUsed | bytes: 0 }} / {{ userVideoQuota }}
index ec0d40b93d925cb51c241dff0e916fb6c7e525b4..16f26dfed2f76293d009c66db369e8154aba8da8 100644 (file)
@@ -1,55 +1,6 @@
 @import '_variables';
 @import '_mixins';
 
-.user {
-  display: flex;
-
-  img {
-    @include avatar(50px);
-
-    margin-right: 15px;
-  }
-
-  .user-info {
-    .user-info-names {
-      display: flex;
-      align-items: center;
-
-      .user-info-display-name {
-        font-size: 20px;
-        font-weight: $font-bold;
-      }
-
-      .user-info-username {
-        margin-left: 7px;
-        position: relative;
-        top: 2px;
-        font-size: 14px;
-        color: #777272;
-      }
-    }
-
-    .user-info-followers {
-      font-size: 15px;
-    }
-  }
-}
-
-.button-file {
-  @include peertube-button-file(160px);
-
-  margin-top: 10px;
-  margin-bottom: 5px;
-}
-
-.file-max-size {
-  display: inline-block;
-  font-size: 13px;
-
-  position: relative;
-  top: -10px;
-}
-
 .user-quota {
   font-size: 15px;
   margin-top: 20px;
index 14293f14c40e51749cbc5c15db00b197b20c644a..164a46a4813ac276d4e9dc1fe4209178493d7bc3 100644 (file)
@@ -13,8 +13,6 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
   styleUrls: [ './my-account-settings.component.scss' ]
 })
 export class MyAccountSettingsComponent implements OnInit {
-  @ViewChild('avatarfileInput') avatarfileInput
-
   user: User = null
   userVideoQuota = '0'
   userVideoQuotaUsed = 0
@@ -48,16 +46,7 @@ export class MyAccountSettingsComponent implements OnInit {
       .subscribe(data => this.userVideoQuotaUsed = data.videoQuotaUsed)
   }
 
-  changeAvatar () {
-    const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
-    if (avatarfile.size > this.maxAvatarSize) {
-      this.notificationsService.error('Error', 'This image is too large.')
-      return
-    }
-
-    const formData = new FormData()
-    formData.append('avatarfile', avatarfile)
-
+  onAvatarChange (formData: FormData) {
     this.userService.changeAvatar(formData)
       .subscribe(
         data => {
@@ -69,12 +58,4 @@ export class MyAccountSettingsComponent implements OnInit {
         err => this.notificationsService.error(this.i18n('Error'), err.message)
       )
   }
-
-  get maxAvatarSize () {
-    return this.serverService.getConfig().avatar.file.size.max
-  }
-
-  get avatarExtensions () {
-    return this.serverService.getConfig().avatar.file.extensions.join(',')
-  }
 }
index 1c08cfdca3a0074510c8065a58f5a5335a07f05a..f7ca2ec4317cd06856f6c74cfda6777dfa825b54 100644 (file)
@@ -1,5 +1,9 @@
+<my-actor-avatar-info
+  *ngIf="isCreation() === false && videoChannelToUpdate"
+  [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)"
+></my-actor-avatar-info>
+
 <div i18n class="form-sub-title" *ngIf="isCreation() === true">Create a video channel</div>
-<div i18n class="form-sub-title" *ngIf="isCreation() === false">Update {{ videoChannel?.displayName }}</div>
 
 <div *ngIf="error" class="alert alert-danger">{{ error }}</div>
 
index 6fbb8ae8bd9035abf04fd0bf69621a51858f9af2..86c2598b79fb0f82382da631b7c70004ade15ee0 100644 (file)
@@ -5,6 +5,11 @@
   margin-bottom: 20px;
 }
 
+my-actor-avatar-info {
+  display: block;
+  margin-bottom: 20px;
+}
+
 input[type=text] {
   @include peertube-input-text(340px);
 
index 1510c5015f9a7e68bd20018006b8dba60f40e67e..6db8ea8d6a41555b43d6651b54d486d359503505 100644 (file)
@@ -1,4 +1,4 @@
-import { Component, OnDestroy, OnInit } from '@angular/core'
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'
 import { ActivatedRoute, Router } from '@angular/router'
 import { NotificationsService } from 'angular2-notifications'
 import { MyAccountVideoChannelEdit } from './my-account-video-channel-edit'
@@ -6,7 +6,7 @@ import { VideoChannelUpdate } from '../../../../../shared/models/videos'
 import { VideoChannelService } from '@app/shared/video-channel/video-channel.service'
 import { Subscription } from 'rxjs'
 import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
-import { AuthService } from '@app/core'
+import { AuthService, ServerService } from '@app/core'
 import { I18n } from '@ngx-translate/i18n-polyfill'
 import { FormValidatorService } from '@app/shared/forms/form-validators/form-validator.service'
 import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators/video-channel-validators.service'
@@ -17,6 +17,8 @@ import { VideoChannelValidatorsService } from '@app/shared/forms/form-validators
   styleUrls: [ './my-account-video-channel-edit.component.scss' ]
 })
 export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelEdit implements OnInit, OnDestroy {
+  @ViewChild('avatarfileInput') avatarfileInput
+
   error: string
 
   private videoChannelToUpdate: VideoChannel
@@ -30,7 +32,8 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
     private router: Router,
     private route: ActivatedRoute,
     private videoChannelService: VideoChannelService,
-    private i18n: I18n
+    private i18n: I18n,
+    private serverService: ServerService
   ) {
     super()
   }
@@ -89,6 +92,27 @@ export class MyAccountVideoChannelUpdateComponent extends MyAccountVideoChannelE
     )
   }
 
+  onAvatarChange (formData: FormData) {
+    this.videoChannelService.changeVideoChannelAvatar(this.videoChannelToUpdate.uuid, formData)
+        .subscribe(
+          data => {
+            this.notificationsService.success(this.i18n('Success'), this.i18n('Avatar changed.'))
+
+            this.videoChannelToUpdate.updateAvatar(data.avatar)
+          },
+
+          err => this.notificationsService.error(this.i18n('Error'), err.message)
+        )
+  }
+
+  get maxAvatarSize () {
+    return this.serverService.getConfig().avatar.file.size.max
+  }
+
+  get avatarExtensions () {
+    return this.serverService.getConfig().avatar.file.extensions.join(',')
+  }
+
   isCreation () {
     return false
   }
index 7e6b8c03ee0d84d589772a4b9bae121226b9f0f5..2088273e6d6e907b673c6806aad0558214f2fdb4 100644 (file)
@@ -10,6 +10,7 @@ import { MyAccountProfileComponent } from '@app/+my-account/my-account-settings/
 import { MyAccountVideoChannelsComponent } from '@app/+my-account/my-account-video-channels/my-account-video-channels.component'
 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 { ActorAvatarInfoComponent } from '@app/+my-account/shared/actor-avatar-info.component'
 
 @NgModule({
   imports: [
@@ -26,7 +27,8 @@ import { MyAccountVideoChannelUpdateComponent } from '@app/+my-account/my-accoun
     MyAccountVideosComponent,
     MyAccountVideoChannelsComponent,
     MyAccountVideoChannelCreateComponent,
-    MyAccountVideoChannelUpdateComponent
+    MyAccountVideoChannelUpdateComponent,
+    ActorAvatarInfoComponent
   ],
 
   exports: [
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.html b/client/src/app/+my-account/shared/actor-avatar-info.component.html
new file mode 100644 (file)
index 0000000..8bdff2f
--- /dev/null
@@ -0,0 +1,19 @@
+<ng-container *ngIf="actor">
+  <div class="actor">
+    <img [src]="actor.avatarUrl" alt="Avatar" />
+
+    <div class="actor-info">
+      <div class="actor-info-names">
+        <div class="actor-info-display-name">{{ actor.displayName }}</div>
+        <div class="actor-info-username">{{ actor.name }}</div>
+      </div>
+      <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
+    </div>
+  </div>
+
+  <div class="button-file">
+    <span i18n>Change the avatar</span>
+    <input #avatarfileInput type="file" name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()" />
+  </div>
+  <div i18n class="file-max-size">(extensions: {{ avatarExtensions }}, max size: {{ maxAvatarSize | bytes }})</div>
+</ng-container>
\ No newline at end of file
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.scss b/client/src/app/+my-account/shared/actor-avatar-info.component.scss
new file mode 100644 (file)
index 0000000..36a792f
--- /dev/null
@@ -0,0 +1,51 @@
+@import '_variables';
+@import '_mixins';
+
+.actor {
+  display: flex;
+
+  img {
+    @include avatar(50px);
+
+    margin-right: 15px;
+  }
+
+  .actor-info {
+    .actor-info-names {
+      display: flex;
+      align-items: center;
+
+      .actor-info-display-name {
+        font-size: 20px;
+        font-weight: $font-bold;
+      }
+
+      .actor-info-username {
+        margin-left: 7px;
+        position: relative;
+        top: 2px;
+        font-size: 14px;
+        color: #777272;
+      }
+    }
+
+    .actor-info-followers {
+      font-size: 15px;
+    }
+  }
+}
+
+.button-file {
+  @include peertube-button-file(160px);
+
+  margin-top: 10px;
+  margin-bottom: 5px;
+}
+
+.file-max-size {
+  display: inline-block;
+  font-size: 13px;
+
+  position: relative;
+  top: -10px;
+}
\ No newline at end of file
diff --git a/client/src/app/+my-account/shared/actor-avatar-info.component.ts b/client/src/app/+my-account/shared/actor-avatar-info.component.ts
new file mode 100644 (file)
index 0000000..e0b25ad
--- /dev/null
@@ -0,0 +1,48 @@
+import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
+import { AuthService } from '../../core'
+import { ServerService } from '../../core/server'
+import { UserService } from '../../shared/users'
+import { NotificationsService } from 'angular2-notifications'
+import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
+import { Account } from '@app/shared/account/account.model'
+
+@Component({
+  selector: 'my-actor-avatar-info',
+  templateUrl: './actor-avatar-info.component.html',
+  styleUrls: [ './actor-avatar-info.component.scss' ]
+})
+export class ActorAvatarInfoComponent {
+  @ViewChild('avatarfileInput') avatarfileInput
+
+  @Input() actor: VideoChannel | Account
+
+  @Output() avatarChange = new EventEmitter<FormData>()
+
+  constructor (
+    private userService: UserService,
+    private authService: AuthService,
+    private serverService: ServerService,
+    private notificationsService: NotificationsService
+  ) {}
+
+  onAvatarChange () {
+    const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
+    if (avatarfile.size > this.maxAvatarSize) {
+      this.notificationsService.error('Error', 'This image is too large.')
+      return
+    }
+
+    const formData = new FormData()
+    formData.append('avatarfile', avatarfile)
+
+    this.avatarChange.emit(formData)
+  }
+
+  get maxAvatarSize () {
+    return this.serverService.getConfig().avatar.file.size.max
+  }
+
+  get avatarExtensions () {
+    return this.serverService.getConfig().avatar.file.extensions.join(',')
+  }
+}
index f820dc3c48e4f62fd614b4ff22eebc288e3b9fb4..811afb4497a38dcf006522a29d5c086551cd38bf 100644 (file)
@@ -45,6 +45,16 @@ export abstract class Actor implements ActorServer {
     this.updatedAt = new Date(hash.updatedAt.toString())
     this.avatar = hash.avatar
 
+    this.updateComputedAttributes()
+  }
+
+  updateAvatar (newAvatar: Avatar) {
+    this.avatar = newAvatar
+
+    this.updateComputedAttributes()
+  }
+
+  private updateComputedAttributes () {
     this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this)
   }
 }
index 60a0f26dfff38db930d7ddc03797e4451de055e4..581ea785964d2dee22ac251d5f2571f0adb7ca91 100644 (file)
@@ -34,7 +34,6 @@ export class User implements UserServerModel {
   account: Account
   videoChannels: VideoChannel[]
   createdAt: Date
-  accountAvatarUrl: string
 
   constructor (hash: UserConstructorHash) {
     this.id = hash.id
@@ -65,8 +64,12 @@ export class User implements UserServerModel {
     if (hash.createdAt !== undefined) {
       this.createdAt = hash.createdAt
     }
+  }
+
+  get accountAvatarUrl () {
+    if (!this.account) return ''
 
-    this.updateComputedAttributes()
+    return this.account.avatarUrl
   }
 
   hasRight (right: UserRight) {
@@ -81,17 +84,9 @@ export class User implements UserServerModel {
     if (obj.account !== undefined) {
       this.account = new Account(obj.account)
     }
-
-    this.updateComputedAttributes()
   }
 
   updateAccountAvatar (newAccountAvatar: Avatar) {
-    this.account.avatar = newAccountAvatar
-
-    this.updateComputedAttributes()
-  }
-
-  private updateComputedAttributes () {
-    this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
+    this.account.updateAvatar(newAccountAvatar)
   }
 }
index 55e4c2a312908ffbb17ed296a712ceac1a18cf66..0b9900208229bc2e5712e2e17beccb1c7225b5e0 100644 (file)
@@ -9,6 +9,7 @@ import { ResultList } from '../../../../../shared'
 import { VideoChannel } from './video-channel.model'
 import { environment } from '../../../environments/environment'
 import { Account } from '@app/shared/account/account.model'
+import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
 
 @Injectable()
 export class VideoChannelService {
@@ -54,6 +55,13 @@ export class VideoChannelService {
                )
   }
 
+  changeVideoChannelAvatar (videoChannelUUID: string, avatarForm: FormData) {
+    const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelUUID + '/avatar/pick'
+
+    return this.authHttp.post<{ avatar: Avatar }>(url, avatarForm)
+               .pipe(catchError(this.restExtractor.handleError))
+  }
+
   removeVideoChannel (videoChannel: VideoChannel) {
     return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.uuid)
                .pipe(
index 7f421dbbb68222d080abc007a47f7adc8c339c68..5c820a22712bce7b8e3b645a087503b715cde41a 100644 (file)
@@ -11,6 +11,7 @@ import { VideoScheduleUpdate } from '../../../../../shared/models/videos/video-s
 export class Video implements VideoServerModel {
   by: string
   accountAvatarUrl: string
+  videoChannelAvatarUrl: string
   createdAt: Date
   updatedAt: Date
   publishedAt: Date
@@ -102,9 +103,11 @@ export class Video implements VideoServerModel {
     this.dislikes = hash.dislikes
     this.nsfw = hash.nsfw
     this.account = hash.account
+    this.channel = hash.channel
 
     this.by = Actor.CREATE_BY_STRING(hash.account.name, hash.account.host)
     this.accountAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.account)
+    this.videoChannelAvatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.channel)
 
     this.category.label = peertubeTranslate(this.category.label, translations)
     this.licence.label = peertubeTranslate(this.licence.label, translations)
index 3ddeda1095f87aedb1f99095513c65bf4d1baf15..9fe1213719fcb889811be44644485285098117ad 100644 (file)
@@ -132,10 +132,10 @@ export class VideoAddComponent extends FormReactive implements OnInit, OnDestroy
     if (!videofile) return
 
     // Cannot upload videos > 4GB for now
-    if (videofile.size > 4 * 1024 * 1024 * 1024) {
-      this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 4GB'))
-      return
-    }
+    // if (videofile.size > 4 * 1024 * 1024 * 1024) {
+    //   this.notificationsService.error(this.i18n('Error'), this.i18n('We are sorry but PeerTube cannot handle videos > 4GB'))
+    //   return
+    // }
 
     const videoQuota = this.authService.getUser().videoQuota
     if (videoQuota !== -1 && (this.userVideoQuotaUsed + videofile.size) > videoQuota) {
index 492568d3cc7a7c6a8c9f5485643319ad238f24ef..cd470e3204874a733f006017c49dfc7a5f8baf64 100644 (file)
@@ -25,6 +25,8 @@
           <div class="video-info-channel">
             <a [routerLink]="[ '/video-channels', video.channel.id ]" i18n-title title="Go the channel page">
               {{ video.channel.displayName }}
+
+              <img [src]="video.videoChannelAvatarUrl" alt="Video channel avatar" />
             </a>
             <!-- Here will be the subscribe button -->
             <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>
index dfabdfa60435fd44a0b95e4b606e7ca60cd25093..4404fa9b2dcc35f1feff0e40093ec784d2ca5f4c 100644 (file)
           &:hover {
             opacity: 0.8;
           }
+
+          img {
+            @include avatar(18px);
+
+            margin: -2px 2px 0 5px;
+          }
         }
 
         my-help {
         img {
           @include avatar(18px);
 
+          margin-top: -2px;
           margin-left: 7px;
         }
       }
index 0af70cadf8b79789088ae99dd22b7c246e8b0630..5d80893288f436aac049f38c20e057bfc4d3162e 100644 (file)
@@ -251,6 +251,10 @@ export enum ScopeNames {
                 attributes: [ 'host' ],
                 model: () => ServerModel.unscoped(),
                 required: false
+              },
+              {
+                model: () => AvatarModel.unscoped(),
+                required: false
               }
             ]
           },
index 7b05e5882df739907e01d177d0c3321b188bb0c7..0980de73b8d972f5b3b67c7294fc95999504ea90 100644 (file)
@@ -14,7 +14,8 @@ import {
   killallServers,
   makeGetRequest,
   makePostBodyRequest,
-  makePutBodyRequest, makeUploadRequest,
+  makePutBodyRequest,
+  makeUploadRequest,
   runServer,
   ServerInfo,
   setAccessTokensToServers,
@@ -22,7 +23,7 @@ import {
 } from '../../utils'
 import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params'
 import { User } from '../../../../shared/models/users'
-import { join } from "path"
+import { join } from 'path'
 
 const expect = chai.expect