aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-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
-rw-r--r--server/controllers/api/users/me.ts18
-rw-r--r--server/controllers/api/video-channel.ts19
-rw-r--r--server/lib/activitypub/actor.ts14
-rw-r--r--server/lib/avatar.ts22
18 files changed, 206 insertions, 26 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;
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 7ab089713..009cf42b7 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -10,7 +10,7 @@ import { CONFIG } from '../../../initializers/config'
10import { MIMETYPES } from '../../../initializers/constants' 10import { MIMETYPES } from '../../../initializers/constants'
11import { sequelizeTypescript } from '../../../initializers/database' 11import { sequelizeTypescript } from '../../../initializers/database'
12import { sendUpdateActor } from '../../../lib/activitypub/send' 12import { sendUpdateActor } from '../../../lib/activitypub/send'
13import { updateActorAvatarFile } from '../../../lib/avatar' 13import { deleteActorAvatarFile, updateActorAvatarFile } from '../../../lib/avatar'
14import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' 14import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
15import { 15import {
16 asyncMiddleware, 16 asyncMiddleware,
@@ -89,6 +89,11 @@ meRouter.post('/me/avatar/pick',
89 asyncRetryTransactionMiddleware(updateMyAvatar) 89 asyncRetryTransactionMiddleware(updateMyAvatar)
90) 90)
91 91
92meRouter.delete('/me/avatar',
93 authenticate,
94 asyncRetryTransactionMiddleware(deleteMyAvatar)
95)
96
92// --------------------------------------------------------------------------- 97// ---------------------------------------------------------------------------
93 98
94export { 99export {
@@ -225,7 +230,16 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
225 230
226 const userAccount = await AccountModel.load(user.Account.id) 231 const userAccount = await AccountModel.load(user.Account.id)
227 232
228 const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount) 233 const avatar = await updateActorAvatarFile(userAccount, avatarPhysicalFile)
229 234
230 return res.json({ avatar: avatar.toFormattedJSON() }) 235 return res.json({ avatar: avatar.toFormattedJSON() })
231} 236}
237
238async function deleteMyAvatar (req: express.Request, res: express.Response) {
239 const user = res.locals.oauth.token.user
240
241 const userAccount = await AccountModel.load(user.Account.id)
242 await deleteActorAvatarFile(userAccount)
243
244 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
245}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index c48e00232..7ac01b0ef 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -13,7 +13,7 @@ import { MIMETYPES } from '../../initializers/constants'
13import { sequelizeTypescript } from '../../initializers/database' 13import { sequelizeTypescript } from '../../initializers/database'
14import { setAsyncActorKeys } from '../../lib/activitypub/actor' 14import { setAsyncActorKeys } from '../../lib/activitypub/actor'
15import { sendUpdateActor } from '../../lib/activitypub/send' 15import { sendUpdateActor } from '../../lib/activitypub/send'
16import { updateActorAvatarFile } from '../../lib/avatar' 16import { deleteActorAvatarFile, updateActorAvatarFile } from '../../lib/avatar'
17import { JobQueue } from '../../lib/job-queue' 17import { JobQueue } from '../../lib/job-queue'
18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' 18import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
19import { 19import {
@@ -70,6 +70,13 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
70 asyncMiddleware(updateVideoChannelAvatar) 70 asyncMiddleware(updateVideoChannelAvatar)
71) 71)
72 72
73videoChannelRouter.delete('/:nameWithHost/avatar',
74 authenticate,
75 // Check the rights
76 asyncMiddleware(videoChannelsUpdateValidator),
77 asyncMiddleware(deleteVideoChannelAvatar)
78)
79
73videoChannelRouter.put('/:nameWithHost', 80videoChannelRouter.put('/:nameWithHost',
74 authenticate, 81 authenticate,
75 asyncMiddleware(videoChannelsUpdateValidator), 82 asyncMiddleware(videoChannelsUpdateValidator),
@@ -133,7 +140,7 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
133 const videoChannel = res.locals.videoChannel 140 const videoChannel = res.locals.videoChannel
134 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON()) 141 const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
135 142
136 const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel) 143 const avatar = await updateActorAvatarFile(videoChannel, avatarPhysicalFile)
137 144
138 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys) 145 auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
139 146
@@ -144,6 +151,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
144 .end() 151 .end()
145} 152}
146 153
154async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
155 const videoChannel = res.locals.videoChannel
156
157 await deleteActorAvatarFile(videoChannel)
158
159 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
160}
161
147async function addVideoChannel (req: express.Request, res: express.Response) { 162async function addVideoChannel (req: express.Request, res: express.Response) {
148 const videoChannelInfo: VideoChannelCreate = req.body 163 const videoChannelInfo: VideoChannelCreate = req.body
149 164
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 52547536c..086d656f9 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -199,6 +199,19 @@ async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo
199 return actor 199 return actor
200} 200}
201 201
202async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) {
203 try {
204 await actor.Avatar.destroy({ transaction: t })
205 } catch (err) {
206 logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
207 }
208
209 actor.avatarId = null
210 actor.Avatar = null
211
212 return actor
213}
214
202async function fetchActorTotalItems (url: string) { 215async function fetchActorTotalItems (url: string) {
203 const options = { 216 const options = {
204 uri: url, 217 uri: url,
@@ -337,6 +350,7 @@ export {
337 fetchActorTotalItems, 350 fetchActorTotalItems,
338 getAvatarInfoIfExists, 351 getAvatarInfoIfExists,
339 updateActorInstance, 352 updateActorInstance,
353 deleteActorAvatarInstance,
340 refreshActorIfNeeded, 354 refreshActorIfNeeded,
341 updateActorAvatarInstance, 355 updateActorAvatarInstance,
342 addFetchOutboxJob 356 addFetchOutboxJob
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index be6657b6f..9d59a4966 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -1,7 +1,7 @@
1import 'multer' 1import 'multer'
2import { sendUpdateActor } from './activitypub/send' 2import { sendUpdateActor } from './activitypub/send'
3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' 3import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
4import { updateActorAvatarInstance } from './activitypub/actor' 4import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
5import { processImage } from '../helpers/image-utils' 5import { processImage } from '../helpers/image-utils'
6import { extname, join } from 'path' 6import { extname, join } from 'path'
7import { retryTransactionWrapper } from '../helpers/database-utils' 7import { retryTransactionWrapper } from '../helpers/database-utils'
@@ -14,8 +14,8 @@ import { downloadImage } from '../helpers/requests'
14import { MAccountDefault, MChannelDefault } from '../types/models' 14import { MAccountDefault, MChannelDefault } from '../types/models'
15 15
16async function updateActorAvatarFile ( 16async function updateActorAvatarFile (
17 avatarPhysicalFile: Express.Multer.File, 17 accountOrChannel: MAccountDefault | MChannelDefault,
18 accountOrChannel: MAccountDefault | MChannelDefault 18 avatarPhysicalFile: Express.Multer.File
19) { 19) {
20 const extension = extname(avatarPhysicalFile.filename) 20 const extension = extname(avatarPhysicalFile.filename)
21 const avatarName = uuidv4() + extension 21 const avatarName = uuidv4() + extension
@@ -40,6 +40,21 @@ async function updateActorAvatarFile (
40 }) 40 })
41} 41}
42 42
43async function deleteActorAvatarFile (
44 accountOrChannel: MAccountDefault | MChannelDefault
45) {
46 return retryTransactionWrapper(() => {
47 return sequelizeTypescript.transaction(async t => {
48 const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t)
49 await updatedActor.save({ transaction: t })
50
51 await sendUpdateActor(accountOrChannel, t)
52
53 return updatedActor.Avatar
54 })
55 })
56}
57
43type DownloadImageQueueTask = { fileUrl: string, filename: string } 58type DownloadImageQueueTask = { fileUrl: string, filename: string }
44 59
45const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { 60const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
@@ -64,5 +79,6 @@ const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVAT
64export { 79export {
65 avatarPathUnsafeCache, 80 avatarPathUnsafeCache,
66 updateActorAvatarFile, 81 updateActorAvatarFile,
82 deleteActorAvatarFile,
67 pushAvatarProcessInQueue 83 pushAvatarProcessInQueue
68} 84}