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 40505b92f97..b0d2ec58dcf 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 @@
Settings
-
+
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 d5d019b35b8..c1636895204 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 {
})
)
}
+
+ onAvatarDelete () {
+ this.userService.deleteAvatar()
+ .subscribe(
+ data => {
+ this.notifier.success($localize`Avatar deleted.`)
+
+ this.user.updateAccountAvatar()
+ },
+
+ (err: HttpErrorResponse) => this.notifier.error(err.message)
+ )
+ }
}
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 5ea00040039..735f9e3baf7 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 @@
-
\ No newline at end of file
+
+
+
+
+
+ Upload a new avatar
+
+
+
+
+ Remove avatar
+
+
\ 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 7118e947178..57c298508ba 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 @@
}
}
}
+
+.actor-img-edit-container ::ng-deep .popover-avatar-info .popover-body {
+ padding: 0;
+
+ .dropdown-item {
+ padding: 6px 10px;
+ border-radius: 4px;
+
+ &:first-child {
+ @include peertube-file;
+ display: block;
+ }
+ }
+}
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 de78a390e1e..451bbbba35c 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 @@
-import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'
+import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'
import { Notifier, ServerService } from '@app/core'
import { getBytes } from '@root-helpers/bytes'
import { ServerConfig } from '@shared/models'
import { VideoChannel } from '../video-channel/video-channel.model'
import { Account } from '../account/account.model'
+import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
+import { Actor } from './actor.model'
@Component({
selector: 'my-actor-avatar-info',
templateUrl: './actor-avatar-info.component.html',
styleUrls: [ './actor-avatar-info.component.scss' ]
})
-export class ActorAvatarInfoComponent implements OnInit {
+export class ActorAvatarInfoComponent implements OnInit, OnChanges {
@ViewChild('avatarfileInput') avatarfileInput: ElementRef
+ @ViewChild('avatarPopover') avatarPopover: NgbPopover
@Input() actor: VideoChannel | Account
@Output() avatarChange = new EventEmitter()
+ @Output() avatarDelete = new EventEmitter()
+ private avatarUrl: string
private serverConfig: ServerConfig
constructor (
@@ -30,19 +35,31 @@ export class ActorAvatarInfoComponent implements OnInit {
.subscribe(config => this.serverConfig = config)
}
- onAvatarChange () {
+ ngOnChanges (changes: SimpleChanges) {
+ if (changes['actor']) {
+ this.avatarUrl = Actor.GET_ACTOR_AVATAR_URL(this.actor)
+ }
+ }
+
+ onAvatarChange (input: HTMLInputElement) {
+ this.avatarfileInput = new ElementRef(input)
+
const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
if (avatarfile.size > this.maxAvatarSize) {
- this.notifier.error('Error', 'This image is too large.')
+ this.notifier.error('Error', $localize`This image is too large.`)
return
}
const formData = new FormData()
formData.append('avatarfile', avatarfile)
-
+ this.avatarPopover?.close()
this.avatarChange.emit(formData)
}
+ deleteAvatar () {
+ this.avatarDelete.emit()
+ }
+
get maxAvatarSize () {
return this.serverConfig.avatar.file.size.max
}
@@ -58,4 +75,8 @@ export class ActorAvatarInfoComponent implements OnInit {
get avatarFormat () {
return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}`
}
+
+ get hasAvatar () {
+ return !!this.avatarUrl
+ }
}
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 4f1f5b65de8..c6a63fe6cea 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 {
this.updateComputedAttributes()
}
+ resetAvatar () {
+ this.avatar = null
+ this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL()
+ }
+
private updateComputedAttributes () {
this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
}
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 64dcf638a70..eff3fad4d99 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 {
.pipe(catchError(err => this.restExtractor.handleError(err)))
}
+ deleteVideoChannelAvatar (videoChannelName: string) {
+ const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannelName + '/avatar'
+
+ return this.authHttp.delete(url)
+ .pipe(
+ map(this.restExtractor.extractDataBool),
+ catchError(err => this.restExtractor.handleError(err))
+ )
+ }
+
removeVideoChannel (videoChannel: VideoChannel) {
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
.pipe(
diff --git a/client/src/sass/include/_mixins.scss b/client/src/sass/include/_mixins.scss
index fecae9fbca5..70f43d63329 100644
--- a/client/src/sass/include/_mixins.scss
+++ b/client/src/sass/include/_mixins.scss
@@ -255,15 +255,12 @@
}
}
-@mixin peertube-button-file ($width) {
+@mixin peertube-file {
position: relative;
overflow: hidden;
display: inline-block;
- width: $width;
min-height: 30px;
- @include peertube-button;
-
input[type=file] {
position: absolute;
top: 0;
@@ -281,6 +278,13 @@
}
}
+@mixin peertube-button-file ($width) {
+ width: $width;
+
+ @include peertube-file;
+ @include peertube-button;
+}
+
@mixin icon ($size) {
display: inline-block;
background-repeat: no-repeat;
diff --git a/server/controllers/api/users/me.ts b/server/controllers/api/users/me.ts
index 3d2a325fe3c..7b2825c3c49 100644
--- a/server/controllers/api/users/me.ts
+++ b/server/controllers/api/users/me.ts
@@ -8,7 +8,7 @@ import { CONFIG } from '../../../initializers/config'
import { MIMETYPES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { sendUpdateActor } from '../../../lib/activitypub/send'
-import { updateActorAvatarFile } from '../../../lib/avatar'
+import { deleteActorAvatarFile, updateActorAvatarFile } from '../../../lib/avatar'
import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
import {
asyncMiddleware,
@@ -86,6 +86,11 @@ meRouter.post('/me/avatar/pick',
asyncRetryTransactionMiddleware(updateMyAvatar)
)
+meRouter.delete('/me/avatar',
+ authenticate,
+ asyncRetryTransactionMiddleware(deleteMyAvatar)
+)
+
// ---------------------------------------------------------------------------
export {
@@ -220,7 +225,16 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
const userAccount = await AccountModel.load(user.Account.id)
- const avatar = await updateActorAvatarFile(avatarPhysicalFile, userAccount)
+ const avatar = await updateActorAvatarFile(userAccount, avatarPhysicalFile)
return res.json({ avatar: avatar.toFormattedJSON() })
}
+
+async function deleteMyAvatar (req: express.Request, res: express.Response) {
+ const user = res.locals.oauth.token.user
+
+ const userAccount = await AccountModel.load(user.Account.id)
+ await deleteActorAvatarFile(userAccount)
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts
index c48e0023293..7ac01b0efe6 100644
--- a/server/controllers/api/video-channel.ts
+++ b/server/controllers/api/video-channel.ts
@@ -13,7 +13,7 @@ import { MIMETYPES } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database'
import { setAsyncActorKeys } from '../../lib/activitypub/actor'
import { sendUpdateActor } from '../../lib/activitypub/send'
-import { updateActorAvatarFile } from '../../lib/avatar'
+import { deleteActorAvatarFile, updateActorAvatarFile } from '../../lib/avatar'
import { JobQueue } from '../../lib/job-queue'
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
import {
@@ -70,6 +70,13 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
asyncMiddleware(updateVideoChannelAvatar)
)
+videoChannelRouter.delete('/:nameWithHost/avatar',
+ authenticate,
+ // Check the rights
+ asyncMiddleware(videoChannelsUpdateValidator),
+ asyncMiddleware(deleteVideoChannelAvatar)
+)
+
videoChannelRouter.put('/:nameWithHost',
authenticate,
asyncMiddleware(videoChannelsUpdateValidator),
@@ -133,7 +140,7 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
const videoChannel = res.locals.videoChannel
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
- const avatar = await updateActorAvatarFile(avatarPhysicalFile, videoChannel)
+ const avatar = await updateActorAvatarFile(videoChannel, avatarPhysicalFile)
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
@@ -144,6 +151,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
.end()
}
+async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
+ const videoChannel = res.locals.videoChannel
+
+ await deleteActorAvatarFile(videoChannel)
+
+ return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
+}
+
async function addVideoChannel (req: express.Request, res: express.Response) {
const videoChannelInfo: VideoChannelCreate = req.body
diff --git a/server/lib/activitypub/actor.ts b/server/lib/activitypub/actor.ts
index 52547536cfe..086d656f9c9 100644
--- a/server/lib/activitypub/actor.ts
+++ b/server/lib/activitypub/actor.ts
@@ -199,6 +199,19 @@ async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo
return actor
}
+async function deleteActorAvatarInstance (actor: MActorDefault, t: Transaction) {
+ try {
+ await actor.Avatar.destroy({ transaction: t })
+ } catch (err) {
+ logger.error('Cannot remove old avatar of actor %s.', actor.url, { err })
+ }
+
+ actor.avatarId = null
+ actor.Avatar = null
+
+ return actor
+}
+
async function fetchActorTotalItems (url: string) {
const options = {
uri: url,
@@ -337,6 +350,7 @@ export {
fetchActorTotalItems,
getAvatarInfoIfExists,
updateActorInstance,
+ deleteActorAvatarInstance,
refreshActorIfNeeded,
updateActorAvatarInstance,
addFetchOutboxJob
diff --git a/server/lib/avatar.ts b/server/lib/avatar.ts
index be6657b6ffb..9d59a496648 100644
--- a/server/lib/avatar.ts
+++ b/server/lib/avatar.ts
@@ -1,7 +1,7 @@
import 'multer'
import { sendUpdateActor } from './activitypub/send'
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
-import { updateActorAvatarInstance } from './activitypub/actor'
+import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor'
import { processImage } from '../helpers/image-utils'
import { extname, join } from 'path'
import { retryTransactionWrapper } from '../helpers/database-utils'
@@ -14,8 +14,8 @@ import { downloadImage } from '../helpers/requests'
import { MAccountDefault, MChannelDefault } from '../types/models'
async function updateActorAvatarFile (
- avatarPhysicalFile: Express.Multer.File,
- accountOrChannel: MAccountDefault | MChannelDefault
+ accountOrChannel: MAccountDefault | MChannelDefault,
+ avatarPhysicalFile: Express.Multer.File
) {
const extension = extname(avatarPhysicalFile.filename)
const avatarName = uuidv4() + extension
@@ -40,6 +40,21 @@ async function updateActorAvatarFile (
})
}
+async function deleteActorAvatarFile (
+ accountOrChannel: MAccountDefault | MChannelDefault
+) {
+ return retryTransactionWrapper(() => {
+ return sequelizeTypescript.transaction(async t => {
+ const updatedActor = await deleteActorAvatarInstance(accountOrChannel.Actor, t)
+ await updatedActor.save({ transaction: t })
+
+ await sendUpdateActor(accountOrChannel, t)
+
+ return updatedActor.Avatar
+ })
+ })
+}
+
type DownloadImageQueueTask = { fileUrl: string, filename: string }
const downloadImageQueue = queue((task, cb) => {
@@ -64,5 +79,6 @@ const avatarPathUnsafeCache = new LRUCache({ max: LRU_CACHE.AVAT
export {
avatarPathUnsafeCache,
updateActorAvatarFile,
+ deleteActorAvatarFile,
pushAvatarProcessInQueue
}