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
This commit is contained in:
parent
75dd1b641f
commit
1ea7da819e
|
@ -3,7 +3,7 @@
|
||||||
<div class="form-group col-12 col-lg-4 col-xl-3"></div>
|
<div class="form-group col-12 col-lg-4 col-xl-3"></div>
|
||||||
|
|
||||||
<div class="form-group col-12 col-lg-8 col-xl-9">
|
<div class="form-group col-12 col-lg-8 col-xl-9">
|
||||||
<my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)"></my-actor-avatar-info>
|
<my-actor-avatar-info [actor]="user.account" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"></my-actor-avatar-info>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
<my-actor-avatar-info
|
<my-actor-avatar-info
|
||||||
*ngIf="!isCreation() && videoChannelToUpdate"
|
*ngIf="!isCreation() && videoChannelToUpdate"
|
||||||
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)"
|
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
|
||||||
></my-actor-avatar-info>
|
></my-actor-avatar-info>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
@ -14,6 +14,7 @@ export abstract class MyVideoChannelEdit extends FormReactive {
|
||||||
|
|
||||||
// We need this method so angular does not complain in child template that doesn't need this
|
// We need this method so angular does not complain in child template that doesn't need this
|
||||||
onAvatarChange (formData: FormData) { /* empty */ }
|
onAvatarChange (formData: FormData) { /* empty */ }
|
||||||
|
onAvatarDelete () { /* empty */ }
|
||||||
|
|
||||||
// Should be implemented by the child
|
// Should be implemented by the child
|
||||||
isBulkUpdateVideosDisplayed () {
|
isBulkUpdateVideosDisplayed () {
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { FormValidatorService } from '@app/shared/shared-forms'
|
||||||
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
|
||||||
import { ServerConfig, VideoChannelUpdate } from '@shared/models'
|
import { ServerConfig, VideoChannelUpdate } from '@shared/models'
|
||||||
import { MyVideoChannelEdit } from './my-video-channel-edit'
|
import { MyVideoChannelEdit } from './my-video-channel-edit'
|
||||||
|
import { HttpErrorResponse } from '@angular/common/http'
|
||||||
|
import { uploadErrorHandler } from '@app/helpers'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-channel-update',
|
selector: 'my-video-channel-update',
|
||||||
|
@ -107,10 +109,27 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
|
||||||
this.videoChannelToUpdate.updateAvatar(data.avatar)
|
this.videoChannelToUpdate.updateAvatar(data.avatar)
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
(err: HttpErrorResponse) => uploadErrorHandler({
|
||||||
|
err,
|
||||||
|
name: $localize`avatar`,
|
||||||
|
notifier: this.notifier
|
||||||
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAvatarDelete () {
|
||||||
|
this.videoChannelService.deleteVideoChannelAvatar(this.videoChannelToUpdate.name)
|
||||||
|
.subscribe(
|
||||||
|
data => {
|
||||||
|
this.notifier.success($localize`Avatar deleted.`)
|
||||||
|
|
||||||
|
this.videoChannelToUpdate.resetAvatar()
|
||||||
|
},
|
||||||
|
|
||||||
|
err => this.notifier.error(err.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get maxAvatarSize () {
|
get maxAvatarSize () {
|
||||||
return this.serverConfig.avatar.file.size.max
|
return this.serverConfig.avatar.file.size.max
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,8 +131,9 @@ export class User implements UserServerModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAccountAvatar (newAccountAvatar: Avatar) {
|
updateAccountAvatar (newAccountAvatar?: Avatar) {
|
||||||
this.account.updateAvatar(newAccountAvatar)
|
if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
|
||||||
|
else this.account.resetAvatar()
|
||||||
}
|
}
|
||||||
|
|
||||||
isUploadDisabled () {
|
isUploadDisabled () {
|
||||||
|
|
|
@ -123,6 +123,16 @@ export class UserService {
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAvatar () {
|
||||||
|
const url = UserService.BASE_USERS_URL + 'me/avatar'
|
||||||
|
|
||||||
|
return this.authHttp.delete(url)
|
||||||
|
.pipe(
|
||||||
|
map(this.restExtractor.extractDataBool),
|
||||||
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
signup (userCreate: UserRegister) {
|
signup (userCreate: UserRegister) {
|
||||||
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -44,6 +44,11 @@ export class Account extends Actor implements ServerAccount {
|
||||||
this.updateComputedAttributes()
|
this.updateComputedAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetAvatar () {
|
||||||
|
this.avatar = null
|
||||||
|
this.avatarUrl = Account.GET_DEFAULT_AVATAR_URL()
|
||||||
|
}
|
||||||
|
|
||||||
private updateComputedAttributes () {
|
private updateComputedAttributes () {
|
||||||
this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this)
|
this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,18 @@
|
||||||
<img [src]="actor.avatarUrl" alt="Avatar" />
|
<img [src]="actor.avatarUrl" alt="Avatar" />
|
||||||
|
|
||||||
<div class="actor-img-edit-container">
|
<div class="actor-img-edit-container">
|
||||||
<div class="actor-img-edit-button" [ngbTooltip]="avatarFormat"
|
|
||||||
placement="right" container="body">
|
<div *ngIf="!hasAvatar" class="actor-img-edit-button" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||||
<my-global-icon iconName="edit"></my-global-icon>
|
<my-global-icon iconName="upload"></my-global-icon>
|
||||||
<label for="avatarfile" i18n>Change your avatar</label>
|
<label class="sr-only" for="avatarfile" i18n>Upload a new avatar</label>
|
||||||
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
|
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="hasAvatar" class="actor-img-edit-button" #avatarPopover="ngbPopover" [ngbPopover]="avatarEditContent" popoverClass="popover-avatar-info" autoClose="outside" placement="right">
|
||||||
|
<my-global-icon iconName="edit"></my-global-icon>
|
||||||
|
<label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -22,4 +28,16 @@
|
||||||
<div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
<div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-template #avatarEditContent>
|
||||||
|
<div class="dropdown-item c-hand" [ngbTooltip]="avatarFormat" placement="right" container="body">
|
||||||
|
<my-global-icon iconName="upload"></my-global-icon>
|
||||||
|
<span for="avatarfile" i18n>Upload a new avatar</span>
|
||||||
|
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange(avatarfileInput)"/>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item c-hand" (click)="deleteAvatar()" (key.enter)="deleteAvatar()">
|
||||||
|
<my-global-icon iconName="delete"></my-global-icon>
|
||||||
|
<span i18n>Remove avatar</span>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 { Notifier, ServerService } from '@app/core'
|
||||||
import { getBytes } from '@root-helpers/bytes'
|
import { getBytes } from '@root-helpers/bytes'
|
||||||
import { ServerConfig } from '@shared/models'
|
import { ServerConfig } from '@shared/models'
|
||||||
import { VideoChannel } from '../video-channel/video-channel.model'
|
import { VideoChannel } from '../video-channel/video-channel.model'
|
||||||
import { Account } from '../account/account.model'
|
import { Account } from '../account/account.model'
|
||||||
|
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Actor } from './actor.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-actor-avatar-info',
|
selector: 'my-actor-avatar-info',
|
||||||
templateUrl: './actor-avatar-info.component.html',
|
templateUrl: './actor-avatar-info.component.html',
|
||||||
styleUrls: [ './actor-avatar-info.component.scss' ]
|
styleUrls: [ './actor-avatar-info.component.scss' ]
|
||||||
})
|
})
|
||||||
export class ActorAvatarInfoComponent implements OnInit {
|
export class ActorAvatarInfoComponent implements OnInit, OnChanges {
|
||||||
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
@ViewChild('avatarfileInput') avatarfileInput: ElementRef<HTMLInputElement>
|
||||||
|
@ViewChild('avatarPopover') avatarPopover: NgbPopover
|
||||||
|
|
||||||
@Input() actor: VideoChannel | Account
|
@Input() actor: VideoChannel | Account
|
||||||
|
|
||||||
@Output() avatarChange = new EventEmitter<FormData>()
|
@Output() avatarChange = new EventEmitter<FormData>()
|
||||||
|
@Output() avatarDelete = new EventEmitter<void>()
|
||||||
|
|
||||||
|
private avatarUrl: string
|
||||||
private serverConfig: ServerConfig
|
private serverConfig: ServerConfig
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
|
@ -30,19 +35,31 @@ export class ActorAvatarInfoComponent implements OnInit {
|
||||||
.subscribe(config => this.serverConfig = config)
|
.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 ]
|
const avatarfile = this.avatarfileInput.nativeElement.files[ 0 ]
|
||||||
if (avatarfile.size > this.maxAvatarSize) {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('avatarfile', avatarfile)
|
formData.append('avatarfile', avatarfile)
|
||||||
|
this.avatarPopover?.close()
|
||||||
this.avatarChange.emit(formData)
|
this.avatarChange.emit(formData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteAvatar () {
|
||||||
|
this.avatarDelete.emit()
|
||||||
|
}
|
||||||
|
|
||||||
get maxAvatarSize () {
|
get maxAvatarSize () {
|
||||||
return this.serverConfig.avatar.file.size.max
|
return this.serverConfig.avatar.file.size.max
|
||||||
}
|
}
|
||||||
|
@ -58,4 +75,8 @@ export class ActorAvatarInfoComponent implements OnInit {
|
||||||
get avatarFormat () {
|
get avatarFormat () {
|
||||||
return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}`
|
return `${$localize`max size`}: 192*192px, ${this.maxAvatarSizeInBytes} ${$localize`extensions`}: ${this.avatarExtensions}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get hasAvatar () {
|
||||||
|
return !!this.avatarUrl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,11 @@ export class VideoChannel extends Actor implements ServerVideoChannel {
|
||||||
this.updateComputedAttributes()
|
this.updateComputedAttributes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetAvatar () {
|
||||||
|
this.avatar = null
|
||||||
|
this.avatarUrl = VideoChannel.GET_DEFAULT_AVATAR_URL()
|
||||||
|
}
|
||||||
|
|
||||||
private updateComputedAttributes () {
|
private updateComputedAttributes () {
|
||||||
this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
|
this.avatarUrl = VideoChannel.GET_ACTOR_AVATAR_URL(this)
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,6 +89,16 @@ export class VideoChannelService {
|
||||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
.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) {
|
removeVideoChannel (videoChannel: VideoChannel) {
|
||||||
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
|
return this.authHttp.delete(VideoChannelService.BASE_VIDEO_CHANNEL_URL + videoChannel.nameWithHost)
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -260,15 +260,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin peertube-button-file ($width) {
|
@mixin peertube-file {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: $width;
|
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
|
|
||||||
@include peertube-button;
|
|
||||||
|
|
||||||
input[type=file] {
|
input[type=file] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -286,6 +283,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin peertube-button-file ($width) {
|
||||||
|
width: $width;
|
||||||
|
|
||||||
|
@include peertube-file;
|
||||||
|
@include peertube-button;
|
||||||
|
}
|
||||||
|
|
||||||
@mixin icon ($size) {
|
@mixin icon ($size) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { CONFIG } from '../../../initializers/config'
|
||||||
import { MIMETYPES } from '../../../initializers/constants'
|
import { MIMETYPES } from '../../../initializers/constants'
|
||||||
import { sequelizeTypescript } from '../../../initializers/database'
|
import { sequelizeTypescript } from '../../../initializers/database'
|
||||||
import { sendUpdateActor } from '../../../lib/activitypub/send'
|
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 { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user'
|
||||||
import {
|
import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
|
@ -89,6 +89,11 @@ meRouter.post('/me/avatar/pick',
|
||||||
asyncRetryTransactionMiddleware(updateMyAvatar)
|
asyncRetryTransactionMiddleware(updateMyAvatar)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
meRouter.delete('/me/avatar',
|
||||||
|
authenticate,
|
||||||
|
asyncRetryTransactionMiddleware(deleteMyAvatar)
|
||||||
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
@ -225,7 +230,16 @@ async function updateMyAvatar (req: express.Request, res: express.Response) {
|
||||||
|
|
||||||
const userAccount = await AccountModel.load(user.Account.id)
|
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() })
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { MIMETYPES } from '../../initializers/constants'
|
||||||
import { sequelizeTypescript } from '../../initializers/database'
|
import { sequelizeTypescript } from '../../initializers/database'
|
||||||
import { setAsyncActorKeys } from '../../lib/activitypub/actor'
|
import { setAsyncActorKeys } from '../../lib/activitypub/actor'
|
||||||
import { sendUpdateActor } from '../../lib/activitypub/send'
|
import { sendUpdateActor } from '../../lib/activitypub/send'
|
||||||
import { updateActorAvatarFile } from '../../lib/avatar'
|
import { deleteActorAvatarFile, updateActorAvatarFile } from '../../lib/avatar'
|
||||||
import { JobQueue } from '../../lib/job-queue'
|
import { JobQueue } from '../../lib/job-queue'
|
||||||
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
|
import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel'
|
||||||
import {
|
import {
|
||||||
|
@ -70,6 +70,13 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick',
|
||||||
asyncMiddleware(updateVideoChannelAvatar)
|
asyncMiddleware(updateVideoChannelAvatar)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
videoChannelRouter.delete('/:nameWithHost/avatar',
|
||||||
|
authenticate,
|
||||||
|
// Check the rights
|
||||||
|
asyncMiddleware(videoChannelsUpdateValidator),
|
||||||
|
asyncMiddleware(deleteVideoChannelAvatar)
|
||||||
|
)
|
||||||
|
|
||||||
videoChannelRouter.put('/:nameWithHost',
|
videoChannelRouter.put('/:nameWithHost',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoChannelsUpdateValidator),
|
asyncMiddleware(videoChannelsUpdateValidator),
|
||||||
|
@ -133,7 +140,7 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
|
||||||
const videoChannel = res.locals.videoChannel
|
const videoChannel = res.locals.videoChannel
|
||||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
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)
|
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||||
|
|
||||||
|
@ -144,6 +151,14 @@ async function updateVideoChannelAvatar (req: express.Request, res: express.Resp
|
||||||
.end()
|
.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) {
|
async function addVideoChannel (req: express.Request, res: express.Response) {
|
||||||
const videoChannelInfo: VideoChannelCreate = req.body
|
const videoChannelInfo: VideoChannelCreate = req.body
|
||||||
|
|
||||||
|
|
|
@ -199,6 +199,19 @@ async function updateActorAvatarInstance (actor: MActorDefault, info: AvatarInfo
|
||||||
return actor
|
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) {
|
async function fetchActorTotalItems (url: string) {
|
||||||
const options = {
|
const options = {
|
||||||
uri: url,
|
uri: url,
|
||||||
|
@ -337,6 +350,7 @@ export {
|
||||||
fetchActorTotalItems,
|
fetchActorTotalItems,
|
||||||
getAvatarInfoIfExists,
|
getAvatarInfoIfExists,
|
||||||
updateActorInstance,
|
updateActorInstance,
|
||||||
|
deleteActorAvatarInstance,
|
||||||
refreshActorIfNeeded,
|
refreshActorIfNeeded,
|
||||||
updateActorAvatarInstance,
|
updateActorAvatarInstance,
|
||||||
addFetchOutboxJob
|
addFetchOutboxJob
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import 'multer'
|
import 'multer'
|
||||||
import { sendUpdateActor } from './activitypub/send'
|
import { sendUpdateActor } from './activitypub/send'
|
||||||
import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants'
|
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 { processImage } from '../helpers/image-utils'
|
||||||
import { extname, join } from 'path'
|
import { extname, join } from 'path'
|
||||||
import { retryTransactionWrapper } from '../helpers/database-utils'
|
import { retryTransactionWrapper } from '../helpers/database-utils'
|
||||||
|
@ -14,8 +14,8 @@ import { downloadImage } from '../helpers/requests'
|
||||||
import { MAccountDefault, MChannelDefault } from '../types/models'
|
import { MAccountDefault, MChannelDefault } from '../types/models'
|
||||||
|
|
||||||
async function updateActorAvatarFile (
|
async function updateActorAvatarFile (
|
||||||
avatarPhysicalFile: Express.Multer.File,
|
accountOrChannel: MAccountDefault | MChannelDefault,
|
||||||
accountOrChannel: MAccountDefault | MChannelDefault
|
avatarPhysicalFile: Express.Multer.File
|
||||||
) {
|
) {
|
||||||
const extension = extname(avatarPhysicalFile.filename)
|
const extension = extname(avatarPhysicalFile.filename)
|
||||||
const avatarName = uuidv4() + extension
|
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 }
|
type DownloadImageQueueTask = { fileUrl: string, filename: string }
|
||||||
|
|
||||||
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
|
const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => {
|
||||||
|
@ -64,5 +79,6 @@ const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVAT
|
||||||
export {
|
export {
|
||||||
avatarPathUnsafeCache,
|
avatarPathUnsafeCache,
|
||||||
updateActorAvatarFile,
|
updateActorAvatarFile,
|
||||||
|
deleteActorAvatarFile,
|
||||||
pushAvatarProcessInQueue
|
pushAvatarProcessInQueue
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user