Skip to content

Commit

Permalink
add ability to remove one's avatar for account and channels (#3467)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rigelk authored Jan 13, 2021
1 parent 75dd1b6 commit 1ea7da8
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ <h1 class="sr-only" i18n>Settings</h1>
<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">
<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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

<my-actor-avatar-info
*ngIf="!isCreation() && videoChannelToUpdate"
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)"
[actor]="videoChannelToUpdate" (avatarChange)="onAvatarChange($event)" (avatarDelete)="onAvatarDelete()"
></my-actor-avatar-info>

<div class="form-group">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
onAvatarChange (formData: FormData) { /* empty */ }
onAvatarDelete () { /* empty */ }

// Should be implemented by the child
isBulkUpdateVideosDisplayed () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoChannel, VideoChannelService } from '@app/shared/shared-main'
import { ServerConfig, VideoChannelUpdate } from '@shared/models'
import { MyVideoChannelEdit } from './my-video-channel-edit'
import { HttpErrorResponse } from '@angular/common/http'
import { uploadErrorHandler } from '@app/helpers'

@Component({
selector: 'my-video-channel-update',
Expand Down Expand Up @@ -107,10 +109,27 @@ export class MyVideoChannelUpdateComponent extends MyVideoChannelEdit implements
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 () {
return this.serverConfig.avatar.file.size.max
}
Expand Down
5 changes: 3 additions & 2 deletions client/src/app/core/users/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ export class User implements UserServerModel {
}
}

updateAccountAvatar (newAccountAvatar: Avatar) {
this.account.updateAvatar(newAccountAvatar)
updateAccountAvatar (newAccountAvatar?: Avatar) {
if (newAccountAvatar) this.account.updateAvatar(newAccountAvatar)
else this.account.resetAvatar()
}

isUploadDisabled () {
Expand Down
10 changes: 10 additions & 0 deletions client/src/app/core/users/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ export class UserService {
.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) {
return this.authHttp.post(UserService.BASE_USERS_URL + 'register', userCreate)
.pipe(
Expand Down
5 changes: 5 additions & 0 deletions client/src/app/shared/shared-main/account/account.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export class Account extends Actor implements ServerAccount {
this.updateComputedAttributes()
}

resetAvatar () {
this.avatar = null
this.avatarUrl = Account.GET_DEFAULT_AVATAR_URL()
}

private updateComputedAttributes () {
this.avatarUrl = Account.GET_ACTOR_AVATAR_URL(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@
<img [src]="actor.avatarUrl" alt="Avatar" />

<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="upload"></my-global-icon>
<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(avatarfileInput)"/>
</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 for="avatarfile" i18n>Change your avatar</label>
<input #avatarfileInput type="file" title=" " name="avatarfile" id="avatarfile" [accept]="avatarExtensions" (change)="onAvatarChange()"/>
<label class="sr-only" for="avatarMenu" i18n>Change your avatar</label>
</div>

</div>
</div>

Expand All @@ -22,4 +28,16 @@
<div i18n class="actor-info-followers">{{ actor.followersCount }} subscribers</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>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>
@ViewChild('avatarPopover') avatarPopover: NgbPopover

@Input() actor: VideoChannel | Account

@Output() avatarChange = new EventEmitter<FormData>()
@Output() avatarDelete = new EventEmitter<void>()

private avatarUrl: string
private serverConfig: ServerConfig

constructor (
Expand All @@ -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
}
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 8 additions & 4 deletions client/src/sass/include/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -260,15 +260,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;
Expand All @@ -286,6 +283,13 @@
}
}

@mixin peertube-button-file ($width) {
width: $width;

@include peertube-file;
@include peertube-button;
}

@mixin icon ($size) {
display: inline-block;
background-repeat: no-repeat;
Expand Down
18 changes: 16 additions & 2 deletions server/controllers/api/users/me.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,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,
Expand Down Expand Up @@ -89,6 +89,11 @@ meRouter.post('/me/avatar/pick',
asyncRetryTransactionMiddleware(updateMyAvatar)
)

meRouter.delete('/me/avatar',
authenticate,
asyncRetryTransactionMiddleware(deleteMyAvatar)
)

// ---------------------------------------------------------------------------

export {
Expand Down Expand Up @@ -225,7 +230,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)
}
Loading

0 comments on commit 1ea7da8

Please sign in to comment.