diff --git a/src/app/update-account/update-account.component.html b/src/app/update-account/update-account.component.html index b0b19c5..9a666c8 100644 --- a/src/app/update-account/update-account.component.html +++ b/src/app/update-account/update-account.component.html @@ -16,16 +16,20 @@
{{snippetService.getSnippet('update-name-modal.t
Account Image URL
- -
- Not a valid url -
+
+
+ + +
+
+ Not a valid url +
+
diff --git a/src/app/update-account/update-account.component.ts b/src/app/update-account/update-account.component.ts index b56d4fc..39eb061 100644 --- a/src/app/update-account/update-account.component.ts +++ b/src/app/update-account/update-account.component.ts @@ -3,6 +3,11 @@ import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import {AccountService, UpdateAccountSettingsOptions} from '../account.service' import {ToastService} from '../toast.service' import {SnippetService} from '../snippet.service' +import {AsyncValidatorFn, FormBuilder} from '@angular/forms' +import {debounceTime, switchMap} from 'rxjs' +import {distinctUntilChanged, first, map} from 'rxjs/operators' +import {fromPromise} from 'rxjs/internal/observable/innerFrom' +import axios from 'axios' @Component({ selector: 'app-update-account', @@ -11,58 +16,51 @@ import {SnippetService} from '../snippet.service' }) export class UpdateAccountComponent { public newName = null - public newImageUrl?: string - public faCircleNotch = faCircleNotch + public isValidatingImageUrl: boolean = false + public readonly faCircleNotch = faCircleNotch + public readonly imageUrlForm = this.formBuilder.group({ + imageUrl: [ + this.accountService.account?.settings.profile?.imageUrl, + { + asyncValidators: [ + this.makeImageUrlValidator(), + ], + updateOn: 'change', + }, + ], + }) - private get newImageUrlTrimmed(): string|undefined { - const newImageUrl = this.newImageUrl?.trim() - if (newImageUrl === '') { - return undefined - } + public get newImageUrl(): string|undefined { + const value = this.imageUrlForm.controls['imageUrl'].getRawValue() - return newImageUrl + return value?.trim() === '' ? undefined : value.trim() } constructor( public accountService: AccountService, public snippetService: SnippetService, private readonly toastService: ToastService, + private readonly formBuilder: FormBuilder, ) { this.newName = this.accountService.account.name - this.newImageUrl = this.accountService.account?.settings.profile?.imageUrl } get isNewNameValid(): boolean { return !this.newNameOrUndefined || this.newNameOrUndefined.length <= 64 } - public get isNewImageUrlValid(): boolean { - if (this.newImageUrlTrimmed === undefined) { - return true - } - - let url: URL - try { - url = new URL(this.newImageUrlTrimmed) - } catch (_) { - return false - } - - return url.protocol === 'https:' && this.newImageUrlTrimmed.length < 1024 - } - get canUpdateAccount(): boolean { if (this.accountService.isUpdatingAccount) { return false } - if (!this.isNewNameValid || !this.isNewImageUrlValid) { + if (!this.isNewNameValid || !this.imageUrlForm.valid) { return false } if (this.accountService.account.name !== this.newNameOrUndefined) { return true } - return this.accountService.account?.settings.profile?.imageUrl !== this.newImageUrlTrimmed + return this.accountService.account?.settings.profile?.imageUrl !== this.newImageUrl } get newNameOrUndefined(): string|undefined { @@ -81,7 +79,7 @@ export class UpdateAccountComponent { willUpdateName = true } let willUpdateImageUrl = false - const newImageUrl = this.newImageUrlTrimmed + const newImageUrl = this.newImageUrl if (newImageUrl !== this.accountService.account?.settings.profile?.imageUrl) { updateOptions.imageUrl = newImageUrl willUpdateImageUrl = true @@ -106,4 +104,37 @@ export class UpdateAccountComponent { this.toastService.showErrorToast(err.message) } } + + private makeImageUrlValidator(): AsyncValidatorFn { + return control => control.valueChanges.pipe( + debounceTime(300), + distinctUntilChanged(), + switchMap(value => fromPromise(this.isValidImageUrl(value))), + map((isValid: boolean) => (isValid ? null : { invalid: true })), + first(), + ) + } + + private async isValidImageUrl(imageUrl: string|undefined): Promise { + imageUrl = imageUrl?.trim() === '' ? undefined : imageUrl.trim() + if (imageUrl === undefined) { + return true + } + + this.isValidatingImageUrl = true + try { + const url = new URL(imageUrl) + + if (url.protocol !== 'https:' || imageUrl.length >= 1024) { + return false + } + await axios.get(imageUrl) + + return true + } catch (_) { + return false + } finally { + this.isValidatingImageUrl = false + } + } }