diff options
author | Rigel Kent <sendmemail@rigelk.eu> | 2021-01-13 09:12:55 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-01-13 09:12:55 +0100 |
commit | 1ea7da819e5bfae7b443ed722c18c4165d101439 (patch) | |
tree | 17cea3786dfb3a59a2ad5559de9ebf106a0440a2 | |
parent | 75dd1b641f987e1e09dbaa3329e08c6e98a858f3 (diff) | |
download | PeerTube-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
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' | |||
11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' | 11 | import { VideoChannel, VideoChannelService } from '@app/shared/shared-main' |
12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' | 12 | import { ServerConfig, VideoChannelUpdate } from '@shared/models' |
13 | import { MyVideoChannelEdit } from './my-video-channel-edit' | 13 | import { MyVideoChannelEdit } from './my-video-channel-edit' |
14 | import { HttpErrorResponse } from '@angular/common/http' | ||
15 | import { 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 @@ | |||
1 | import { Component, ElementRef, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core' | 1 | import { Component, ElementRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core' |
2 | import { Notifier, ServerService } from '@app/core' | 2 | import { Notifier, ServerService } from '@app/core' |
3 | import { getBytes } from '@root-helpers/bytes' | 3 | import { getBytes } from '@root-helpers/bytes' |
4 | import { ServerConfig } from '@shared/models' | 4 | import { ServerConfig } from '@shared/models' |
5 | import { VideoChannel } from '../video-channel/video-channel.model' | 5 | import { VideoChannel } from '../video-channel/video-channel.model' |
6 | import { Account } from '../account/account.model' | 6 | import { Account } from '../account/account.model' |
7 | import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' | ||
8 | import { 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 | }) |
13 | export class ActorAvatarInfoComponent implements OnInit { | 15 | export 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' | |||
10 | import { MIMETYPES } from '../../../initializers/constants' | 10 | import { MIMETYPES } from '../../../initializers/constants' |
11 | import { sequelizeTypescript } from '../../../initializers/database' | 11 | import { sequelizeTypescript } from '../../../initializers/database' |
12 | import { sendUpdateActor } from '../../../lib/activitypub/send' | 12 | import { sendUpdateActor } from '../../../lib/activitypub/send' |
13 | import { updateActorAvatarFile } from '../../../lib/avatar' | 13 | import { deleteActorAvatarFile, updateActorAvatarFile } from '../../../lib/avatar' |
14 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' | 14 | import { getOriginalVideoFileTotalDailyFromUser, getOriginalVideoFileTotalFromUser, sendVerifyUserEmail } from '../../../lib/user' |
15 | import { | 15 | import { |
16 | asyncMiddleware, | 16 | asyncMiddleware, |
@@ -89,6 +89,11 @@ meRouter.post('/me/avatar/pick', | |||
89 | asyncRetryTransactionMiddleware(updateMyAvatar) | 89 | asyncRetryTransactionMiddleware(updateMyAvatar) |
90 | ) | 90 | ) |
91 | 91 | ||
92 | meRouter.delete('/me/avatar', | ||
93 | authenticate, | ||
94 | asyncRetryTransactionMiddleware(deleteMyAvatar) | ||
95 | ) | ||
96 | |||
92 | // --------------------------------------------------------------------------- | 97 | // --------------------------------------------------------------------------- |
93 | 98 | ||
94 | export { | 99 | export { |
@@ -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 | |||
238 | async 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' | |||
13 | import { sequelizeTypescript } from '../../initializers/database' | 13 | import { sequelizeTypescript } from '../../initializers/database' |
14 | import { setAsyncActorKeys } from '../../lib/activitypub/actor' | 14 | import { setAsyncActorKeys } from '../../lib/activitypub/actor' |
15 | import { sendUpdateActor } from '../../lib/activitypub/send' | 15 | import { sendUpdateActor } from '../../lib/activitypub/send' |
16 | import { updateActorAvatarFile } from '../../lib/avatar' | 16 | import { deleteActorAvatarFile, updateActorAvatarFile } from '../../lib/avatar' |
17 | import { JobQueue } from '../../lib/job-queue' | 17 | import { JobQueue } from '../../lib/job-queue' |
18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' | 18 | import { createLocalVideoChannel, federateAllVideosOfChannel } from '../../lib/video-channel' |
19 | import { | 19 | import { |
@@ -70,6 +70,13 @@ videoChannelRouter.post('/:nameWithHost/avatar/pick', | |||
70 | asyncMiddleware(updateVideoChannelAvatar) | 70 | asyncMiddleware(updateVideoChannelAvatar) |
71 | ) | 71 | ) |
72 | 72 | ||
73 | videoChannelRouter.delete('/:nameWithHost/avatar', | ||
74 | authenticate, | ||
75 | // Check the rights | ||
76 | asyncMiddleware(videoChannelsUpdateValidator), | ||
77 | asyncMiddleware(deleteVideoChannelAvatar) | ||
78 | ) | ||
79 | |||
73 | videoChannelRouter.put('/:nameWithHost', | 80 | videoChannelRouter.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 | ||
154 | async 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 | |||
147 | async function addVideoChannel (req: express.Request, res: express.Response) { | 162 | async 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 | ||
202 | async 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 | |||
202 | async function fetchActorTotalItems (url: string) { | 215 | async 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 @@ | |||
1 | import 'multer' | 1 | import 'multer' |
2 | import { sendUpdateActor } from './activitypub/send' | 2 | import { sendUpdateActor } from './activitypub/send' |
3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' | 3 | import { AVATARS_SIZE, LRU_CACHE, QUEUE_CONCURRENCY } from '../initializers/constants' |
4 | import { updateActorAvatarInstance } from './activitypub/actor' | 4 | import { updateActorAvatarInstance, deleteActorAvatarInstance } from './activitypub/actor' |
5 | import { processImage } from '../helpers/image-utils' | 5 | import { processImage } from '../helpers/image-utils' |
6 | import { extname, join } from 'path' | 6 | import { extname, join } from 'path' |
7 | import { retryTransactionWrapper } from '../helpers/database-utils' | 7 | import { retryTransactionWrapper } from '../helpers/database-utils' |
@@ -14,8 +14,8 @@ import { downloadImage } from '../helpers/requests' | |||
14 | import { MAccountDefault, MChannelDefault } from '../types/models' | 14 | import { MAccountDefault, MChannelDefault } from '../types/models' |
15 | 15 | ||
16 | async function updateActorAvatarFile ( | 16 | async 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 | ||
43 | async 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 | |||
43 | type DownloadImageQueueTask = { fileUrl: string, filename: string } | 58 | type DownloadImageQueueTask = { fileUrl: string, filename: string } |
44 | 59 | ||
45 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { | 60 | const downloadImageQueue = queue<DownloadImageQueueTask, Error>((task, cb) => { |
@@ -64,5 +79,6 @@ const avatarPathUnsafeCache = new LRUCache<string, string>({ max: LRU_CACHE.AVAT | |||
64 | export { | 79 | export { |
65 | avatarPathUnsafeCache, | 80 | avatarPathUnsafeCache, |
66 | updateActorAvatarFile, | 81 | updateActorAvatarFile, |
82 | deleteActorAvatarFile, | ||
67 | pushAvatarProcessInQueue | 83 | pushAvatarProcessInQueue |
68 | } | 84 | } |