aboutsummaryrefslogtreecommitdiffhomepage
path: root/client/src
diff options
context:
space:
mode:
authorRigel Kent <sendmemail@rigelk.eu>2021-01-13 09:12:55 +0100
committerGitHub <noreply@github.com>2021-01-13 09:12:55 +0100
commit1ea7da819e5bfae7b443ed722c18c4165d101439 (patch)
tree17cea3786dfb3a59a2ad5559de9ebf106a0440a2 /client/src
parent75dd1b641f987e1e09dbaa3329e08c6e98a858f3 (diff)
downloadPeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.tar.gz
PeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.tar.zst
PeerTube-1ea7da819e5bfae7b443ed722c18c4165d101439.zip
add ability to remove one's avatar for account and channels (#3467)
* add ability to remove one's avatar for account and channels * add ability to remove one's avatar for account and channels * only display avatar edition options after input change
Diffstat (limited to 'client/src')
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.html2
-rw-r--r--client/src/app/+my-account/my-account-settings/my-account-settings.component.ts13
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html2
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts1
-rw-r--r--client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts21
-rw-r--r--client/src/app/core/users/user.model.ts5
-rw-r--r--client/src/app/core/users/user.service.ts10
-rw-r--r--client/src/app/shared/shared-main/account/account.model.ts5
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.html28
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.scss14
-rw-r--r--client/src/app/shared/shared-main/account/actor-avatar-info.component.ts31
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.model.ts5
-rw-r--r--client/src/app/shared/shared-main/video-channel/video-channel.service.ts10
-rw-r--r--client/src/sass/include/_mixins.scss12
14 files changed, 140 insertions, 19 deletions
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
index 40505b92f..b0d2ec58d 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.html
@@ -3,7 +3,7 @@
3 <div class="form-group col-12 col-lg-4 col-xl-3"></div> 3 <div class="form-group col-12 col-lg-4 col-xl-3"></div>
4 4
5 <div class="form-group col-12 col-lg-8 col-xl-9"> 5 <div class="form-group col-12 col-lg-8 col-xl-9">
6 <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)"></my-actor-avatar-info> 6 <my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info>
7 </div> 7 </div>
8</div> 8</div>
9 9
diff --git a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
index d5d019b35..c16368952 100644
--- a/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
+++ b/client/src/app/+my-account/my-account-settings/my-account-settings.component.ts
@@ -53,4 +53,17 @@ export class MyAccountSettingsComponent implements OnInit, AfterViewChecked {
53 }) 53 })
54 ) 54 )
55 } 55 }
56
57 onAvatarDelete () {
58 this.userService.deleteAvatar()
59 .subscribe(
60 data => {
61 this.notifier.success($localize`Avatar deleted.`)
62
63 this.user.updateAccountAvatar()
64 },
65
66 (err: HttpErrorResponse) => this.notifier.error(err.message)
67 )
68 }
56} 69}
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
index 5ea000400..735f9e3ba 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.component.html
@@ -46,7 +46,7 @@
46 46
47 <my-actor-avatar-info 47 <my-actor-avatar-info
48 *ngIf="!isCreation() && videoChannelToUpdate" 48 *ngIf="!isCreation() && videoChannelToUpdate"
49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" 49 [actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
50 ></my-actor-avatar-info> 50 ></my-actor-avatar-info>
51 51
52 <div class="form-group"> 52 <div class="form-group">
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
index 09db0df9d..3e20a27ee 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-edit.ts
@@ -14,6 +14,7 @@ export abstract class MyVideoChannelEdit extends FormReactive {
14 14
15 // We need this method so angular does not complain in child template that doesn't need this 15 // We need this method so angular does not complain in child template that doesn't need this
16 onAvatarChange (formData: FormData) { /* empty */ } 16 onAvatarChange (formData: FormData) { /* empty */ }
17 onAvatarDelete () { /* empty */ }
17 18
18 // Should be implemented by the child 19 // Should be implemented by the child
19 isBulkUpdateVideosDisplayed () { 20 isBulkUpdateVideosDisplayed () {
diff --git a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
index c6cb5ade6..6cd1ff503 100644
--- a/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
+++ b/client/src/app/+my-library/+my-video-channels/my-video-channel-update.component.ts
@@ -11,6 +11,8 @@ import { FormValidatorService } from '@app/shared/shared-forms'
11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' 11import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
12import { ServerConfig, VideoChannelUpdate } from '@shared/models' 12import { ServerConfig, VideoChannelUpdate } from '@shared/models'
13import { MyVideoChannelEdit } from './my-video-channel-edit' 13import { MyVideoChannelEdit } from './my-video-channel-edit'
14import { HttpErrorResponse } from '@angular/common/http'
15import { uploadErrorHandler } from '@app/helpers'
14 16
15@Component({ 17@Component({
16 selector: 'my-video-channel-update', 18 selector: 'my-video-channel-update',
@@ -107,10 +109,27 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
107 this.videoChannelToUpdate.updateAvatar(data.avatar) 109 this.videoChannelToUpdate.updateAvatar(data.avatar)
108 }, 110 },
109 111
110 err => this.notifier.error(err.message) 112 (err: HttpErrorResponse) => uploadErrorHandler({
113 err,
114 name: $localize`avatar`,
115 notifier: this.notifier
116 })
111 ) 117 )
112 } 118 }
113 119
120 onAvatarDelete () {
121 this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name)
122 .subscribe(
123 data => {
124 this.notifier.success($localize`Avatar deleted.`)
125
126 this.videoChannelToUpdate.resetAvatar()
127 },
128
129 err => this.notifier.error(err.message)
130 )
131 }
132
114 get maxAvatarSize () { 133 get maxAvatarSize () {
115 return this.serverConfig.avatar.file.size.max 134 return this.serverConfig.avatar.file.size.max
116 } 135 }
diff --git a/client/src/app/core/users/user.model.ts b/client/src/app/core/users/user.model.ts
index 7c9569ed4..15a4f7f82 100644
--- a/client/src/app/core/users/user.model.ts
+++ b/client/src/app/core/users/user.model.ts
@@ -131,8 +131,9 @@ export class User implements UserServerModel {
131 } 131 }
132 } 132 }
133 133
134 updateAccountAvatar (newAccountAvatar: Avatar) { 134 updateAccountAvatar (newAccountAvatar?: Avatar) {
135 this.account.updateAvatar(newAccountAvatar) 135 if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
136 else this.account.resetAvatar()
136 } 137 }
137 138
138 isUploadDisabled () { 139 isUploadDisabled () {
diff --git a/client/src/app/core/users/user.service.ts b/client/src/app/core/users/user.service.ts
index 2f3945169..99ed3b407 100644
--- a/client/src/app/core/users/user.service.ts
+++ b/client/src/app/core/users/user.service.ts
@@ -123,6 +123,16 @@ export class UserService {
123 .pipe(catchError(err => this.restExtractor.handleError(err))) 123 .pipe(catchError(err => this.restExtractor.handleError(err)))
124 } 124 }
125 125
126 deleteAvatar () {
127 const url = UserService.BASE_USERS_URL + 'me/avatar'
128
129 return this.authHttp.delete(url)
130 .pipe(
131 map(this.restExtractor.extractDataBool),
132 catchError(err => this.restExtractor.handleError(err))
133 )
134 }
135
126 signup (userCreate: UserRegister) { 136 signup (userCreate: UserRegister) {
127 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate) 137 return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
128 .pipe( 138 .pipe(
diff --git a/client/src/app/shared/shared-main/account/account.model.ts b/client/src/app/shared/shared-main/account/account.model.ts
index b3dc6cfe5..b71a893d1 100644
--- a/client/src/app/shared/shared-main/account/account.model.ts
+++ b/client/src/app/shared/shared-main/account/account.model.ts
@@ -44,6 +44,11 @@ export class Account extends Actor implements ServerAccount {
44 this.updateComputedAttributes() 44 this.updateComputedAttributes()
45 } 45 }
46 46
47 resetAvatar () {
48 this.avatar = null
49 this.avatarUrl = Account.GET_DEFAULT_AVATAR_URL()
50 }
51
47 private updateComputedAttributes () { 52 private updateComputedAttributes () {
48 this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this) 53 this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this)
49 } 54 }
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
index e63d8de2d..a34d27b26 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
+++ b/client/src/app/shared/shared-main/account/actor-avatar-info.component.html
@@ -4,12 +4,18 @@
4 <img [src]="actor.avatarUrl" alt="Avatar" /> 4 <img [src]="actor.avatarUrl" alt="Avatar" />
5 5
6 <div class="actor-img-edit-container"> 6 <div class="actor-img-edit-container">
7 <div class="actor-img-edit-button" [ngbTooltip]="avatarFormat" 7
8 placement="right" container="body"> 8 <div *ngIf="!hasAvatar" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
9 <my-global-icon iconName="upload"></my-global-icon>
10 <label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
11 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
12 </div>
13
14 <div *ngIf="hasAvatar" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
9 <my-global-icon iconName="edit"></my-global-icon> 15 <my-global-icon iconName="edit"></my-global-icon>
10 <label for="avatarfile" i18n>Change your avatar</label> 16 <label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
11 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
12 </div> 17 </div>
18
13 </div> 19 </div>
14 </div> 20 </div>
15 21
@@ -22,4 +28,16 @@
22 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div> 28 <div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
23 </div> 29 </div>
24 </div> 30 </div>
25</ng-container> \ No newline at end of file 31</ng-container>
32
33<ng-template #avatarEditContent>
34 <div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
35 <my-global-icon iconName="upload"></my-global-icon>
36 <span for="avatarfile" i18n>Upload a new avatar</span>
37 <input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
38 </div>
39 <div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
40 <my-global-icon iconName="delete"></my-global-icon>
41 <span i18n>Remove avatar</span>
42 </div>
43</ng-template> \ No newline at end of file
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
index 7118e9471..57c298508 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
+++ b/client/src/app/shared/shared-main/account/actor-avatar-info.component.scss
@@ -70,3 +70,17 @@
70 } 70 }
71 } 71 }
72} 72}
73
74.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
75 padding: 0;
76
77 .dropdown-item {
78 padding: 6px 10px;
79 border-radius: 4px;
80
81 &:first-child {
82 @include peertube-file;
83 display: block;
84 }
85 }
86}
diff --git a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts b/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
index de78a390e..451bbbba3 100644
--- a/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
+++ b/client/src/app/shared/shared-main/account/actor-avatar-info.component.ts
@@ -1,22 +1,27 @@
1import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' 1import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
2import { Notifier, ServerService } from '@app/core' 2import { Notifier, ServerService } from '@app/core'
3import { getBytes } from '@root-helpers/bytes' 3import { getBytes } from '@root-helpers/bytes'
4import { ServerConfig } from '@shared/models' 4import { ServerConfig } from '@shared/models'
5import { VideoChannel } from '../video-channel/video-channel.model' 5import { VideoChannel } from '../video-channel/video-channel.model'
6import { Account } from '../account/account.model' 6import { Account } from '../account/account.model'
7import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
8import { Actor } from './actor.model'
7 9
8@Component({ 10@Component({
9 selector: 'my-actor-avatar-info', 11 selector: 'my-actor-avatar-info',
10 templateUrl: './actor-avatar-info.component.html', 12 templateUrl: './actor-avatar-info.component.html',
11 styleUrls: [ './actor-avatar-info.component.scss' ] 13 styleUrls: [ './actor-avatar-info.component.scss' ]
12}) 14})
13export class ActorAvatarInfoComponent implements OnInit { 15export class ActorAvatarInfoComponent implements OnInit, OnChanges {
14 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement> 16 @ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
17 @ViewChild('avatarPopover') avatarPopover: NgbPopover
15 18
16 @Input() actor: VideoChannel | Account 19 @Input() actor: VideoChannel | Account
17 20
18 @Output() avatarChange = new EventEmitter<FormData>() 21 @Output() avatarChange = new EventEmitter<FormData>()
22 @Output() avatarDelete = new EventEmitter<void>()
19 23
24 private avatarUrl: string
20 private serverConfig: ServerConfig 25 private serverConfig: ServerConfig
21 26
22 constructor ( 27 constructor (
@@ -30,19 +35,31 @@ export class ActorAvatarInfoComponent implements OnInit {
30 .subscribe(config => this.serverConfig = config) 35 .subscribe(config => this.serverConfig = config)
31 } 36 }
32 37
33 onAvatarChange () { 38 ngOnChanges (changes: SimpleChanges) {
39 if (changes['actor']) {
40 this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
41 }
42 }
43
44 onAvatarChange (input: HTMLInputElement) {
45 this.avatarfileInput = new ElementRef(input)
46
34 const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ] 47 const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
35 if (avatarfile.size > this.maxAvatarSize) { 48 if (avatarfile.size > this.maxAvatarSize) {
36 this.notifier.error('Error', 'This image is too large.') 49 this.notifier.error('Error', $localize`This image is too large.`)
37 return 50 return
38 } 51 }
39 52
40 const formData = new FormData() 53 const formData = new FormData()
41 formData.append('avatarfile', avatarfile) 54 formData.append('avatarfile', avatarfile)
42 55 this.avatarPopover?.close()
43 this.avatarChange.emit(formData) 56 this.avatarChange.emit(formData)
44 } 57 }
45 58
59 deleteAvatar () {
60 this.avatarDelete.emit()
61 }
62
46 get maxAvatarSize () { 63 get maxAvatarSize () {
47 return this.serverConfig.avatar.file.size.max 64 return this.serverConfig.avatar.file.size.max
48 } 65 }
@@ -58,4 +75,8 @@ export class ActorAvatarInfoComponent implements OnInit {
58 get avatarFormat () { 75 get avatarFormat () {
59 return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}` 76 return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}`
60 } 77 }
78
79 get hasAvatar () {
80 return !!this.avatarUrl
81 }
61} 82}
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
index 4f1f5b65d..c6a63fe6c 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.model.ts
@@ -56,6 +56,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
56 this.updateComputedAttributes() 56 this.updateComputedAttributes()
57 } 57 }
58 58
59 resetAvatar () {
60 this.avatar = null
61 this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL()
62 }
63
59 private updateComputedAttributes () { 64 private updateComputedAttributes () {
60 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this) 65 this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
61 } 66 }
diff --git a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
index 64dcf638a..eff3fad4d 100644
--- a/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
+++ b/client/src/app/shared/shared-main/video-channel/video-channel.service.ts
@@ -89,6 +89,16 @@ export class VideoChannelService {
89 .pipe(catchError(err => this.restExtractor.handleError(err))) 89 .pipe(catchError(err => this.restExtractor.handleError(err)))
90 } 90 }
91 91
92 deleteVideoChannelAvatar (videoChannelName: string) {
93 const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar'
94
95 return this.authHttp.delete(url)
96 .pipe(
97 map(this.restExtractor.extractDataBool),
98 catchError(err => this.restExtractor.handleError(err))
99 )
100 }
101
92 removeVideoChannel (videoChannel: VideoChannel) { 102 removeVideoChannel (videoChannel: VideoChannel) {
93 return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost) 103 return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
94 .pipe( 104 .pipe(
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index 0ce22354e..51cf4c3ed 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -260,15 +260,12 @@
260 } 260 }
261} 261}
262 262
263@mixin peertube-button-file ($width) { 263@mixin peertube-file {
264 position: relative; 264 position: relative;
265 overflow: hidden; 265 overflow: hidden;
266 display: inline-block; 266 display: inline-block;
267 width: $width;
268 min-height: 30px; 267 min-height: 30px;
269 268
270 @include peertube-button;
271
272 input[type=file] { 269 input[type=file] {
273 position: absolute; 270 position: absolute;
274 top: 0; 271 top: 0;
@@ -286,6 +283,13 @@
286 } 283 }
287} 284}
288 285
286@mixin peertube-button-file ($width) {
287 width: $width;
288
289 @include peertube-file;
290 @include peertube-button;
291}
292
289@mixin icon ($size) { 293@mixin icon ($size) {
290 display: inline-block; 294 display: inline-block;
291 background-repeat: no-repeat; 295 background-repeat: no-repeat;