From fb4e9f7de1ddd698559da78f4e634112d5cd57d6 Mon Sep 17 00:00:00 2001 From: Jesse Brault Date: Sun, 15 Feb 2026 16:40:51 -0600 Subject: [PATCH] MME-29 Remove tanstack query stuff from ImageService. --- .../recipe-page-content.html | 8 +-- .../recipe-page-content.ts | 43 +++++++------- .../recipe-card/recipe-card.html | 10 ++-- .../recipe-card/recipe-card.ts | 42 ++++++------- src/app/shared/services/ImageService.ts | 59 ++++++++----------- 5 files changed, 77 insertions(+), 85 deletions(-) diff --git a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html index 05b7f64..4a0f0ed 100644 --- a/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html +++ b/src/app/pages/recipe-page/recipe-page-content/recipe-page-content.html @@ -41,14 +41,14 @@ } - @if (mainImageQuery.isLoading()) { + @if (loadingMainImage()) { - } @else if (mainImageQuery.isError()) { + } @else if (loadMainImageError()) {

There was an error loading the main image.

- } @else if (mainImageQuery.isEnabled()) { + } @else if (mainImage()) { (); private readonly imageService = inject(ImageService); @@ -34,23 +33,27 @@ export class RecipePageContent { protected readonly isLoggedIn = computed(() => !!this.authService.accessToken()); protected readonly isOwner = computed(() => !!this.recipeView().isOwner); - protected readonly mainImageQuery = injectQuery(() => { - let options: Partial< - CreateQueryOptions - > = {}; - const mainImageView = this.recipeView().recipe.mainImage; - if (mainImageView) { - options = this.imageService.getImage(mainImageView); - } else { - options.enabled = false; + protected readonly loadingMainImage = signal(false); + protected readonly loadMainImageError = signal(null); + protected readonly mainImage = signal(null); + + public ngOnInit(): void { + const recipe = this.recipeView().recipe; + if (recipe.mainImage) { + this.loadingMainImage.set(true); + this.imageService.getImageBlobUrl(recipe.mainImage.owner.username, recipe.mainImage.filename).subscribe({ + next: (blobUrl) => { + this.loadingMainImage.set(false); + this.mainImage.set(blobUrl); + }, + error: (e) => { + this.loadingMainImage.set(false); + this.loadMainImageError.set(e); + console.error(e); + }, + }); } - return options as CreateQueryOptions< - ImageViewWithBlobUrl, - Error, - ImageViewWithBlobUrl, - [string, string, string] - >; - }); + } protected readonly starMutation = injectMutation(() => ({ mutationFn: () => this.recipeService.toggleStar(this.recipeView()), diff --git a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.html b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.html index 75405e1..edf8c29 100644 --- a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.html +++ b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.html @@ -1,12 +1,12 @@ @let recipe = this.recipe();
- @if (mainImage.isLoading()) { + @if (loadingMainImage()) { - } @else if (mainImage.isError()) { -

There was an error loading the image.

- } @else if (mainImage.isEnabled()) { - + } @else if (loadMainImageError()) { +

Error

+ } @else if (mainImage()) { + } @else {
diff --git a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts index 3737c6c..21c6d14 100644 --- a/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts +++ b/src/app/shared/components/recipe-card-grid/recipe-card/recipe-card.ts @@ -1,12 +1,10 @@ -import { Component, computed, inject, input } from '@angular/core'; +import { Component, computed, inject, input, OnInit, signal } from '@angular/core'; import { RecipeInfoView } from '../../../models/Recipe.model'; import { RouterLink } from '@angular/router'; -import { CreateQueryOptions, injectQuery } from '@tanstack/angular-query-experimental'; import { ImageService } from '../../../services/ImageService'; import { faGlobe, faLock, faStar, faUser } from '@fortawesome/free-solid-svg-icons'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { Logo } from '../../logo/logo'; -import { ImageViewWithBlobUrl } from '../../../client-models/ImageViewWithBlobUrl'; import { Spinner } from '../../spinner/spinner'; @Component({ @@ -15,7 +13,7 @@ import { Spinner } from '../../spinner/spinner'; templateUrl: './recipe-card.html', styleUrl: './recipe-card.css', }) -export class RecipeCard { +export class RecipeCard implements OnInit { public recipe = input.required(); protected readonly recipePageLink = computed(() => { @@ -30,21 +28,25 @@ export class RecipeCard { private readonly imageService = inject(ImageService); - protected readonly mainImage = injectQuery(() => { - const mainImageView = this.recipe().mainImage; - let options: Partial< - CreateQueryOptions - > = {}; - if (mainImageView) { - options = this.imageService.getImage(mainImageView); - } else { - options.enabled = false; + protected readonly loadingMainImage = signal(false); + protected readonly loadMainImageError = signal(null); + protected readonly mainImage = signal(null); + + public ngOnInit(): void { + const recipe = this.recipe(); + if (recipe.mainImage) { + this.loadingMainImage.set(true); + this.imageService.getImageBlobUrl(recipe.mainImage.owner.username, recipe.mainImage.filename).subscribe({ + next: (blobUrl) => { + this.loadingMainImage.set(false); + this.mainImage.set(blobUrl); + }, + error: (e) => { + this.loadingMainImage.set(false); + this.loadMainImageError.set(e); + console.error(e); + }, + }); } - return options as CreateQueryOptions< - ImageViewWithBlobUrl, - Error, - ImageViewWithBlobUrl, - [string, string, string] - >; - }); + } } diff --git a/src/app/shared/services/ImageService.ts b/src/app/shared/services/ImageService.ts index 4f89cd2..6b94cb1 100644 --- a/src/app/shared/services/ImageService.ts +++ b/src/app/shared/services/ImageService.ts @@ -1,11 +1,10 @@ import { inject, Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { firstValueFrom, from, map, mergeMap, Observable, tap, toArray } from 'rxjs'; +import { firstValueFrom, from, map, mergeMap, Observable, shareReplay, toArray } from 'rxjs'; import { EndpointService } from './EndpointService'; import { ImageView } from '../models/ImageView.model'; import { SliceView } from '../models/SliceView.model'; import { QueryParams } from '../models/Query.model'; -import { QueryClient, QueryOptions, queryOptions } from '@tanstack/angular-query-experimental'; import { ImageViewWithBlobUrl } from '../client-models/ImageViewWithBlobUrl'; import { WithStringDates } from '../util'; import { ImageUpdateBody } from '../bodies/ImageUpdateBody'; @@ -31,7 +30,12 @@ export class ImageService { private readonly httpClient = inject(HttpClient); private readonly endpointService = inject(EndpointService); - private readonly queryClient = inject(QueryClient); + + private readonly imageBlobUrls: Map> = new Map(); + + private getUsernameFilenameKey(username: string, filename: string): string { + return username + '/' + filename; + } public hydrateImageView(rawImageView: WithStringDates): ImageView { return { @@ -90,32 +94,23 @@ export class ImageService { .pipe(map((res) => res.count)); } - public getImageViewWithBlobUrl(imageView: ImageView): Promise { - return firstValueFrom( - this.httpClient - .get(this.endpointService.getUrl('images', [imageView.owner.username, imageView.filename]), { - responseType: 'blob', - }) - .pipe( - map((blob) => URL.createObjectURL(blob)), - map( - (blobUrl) => - ({ - ...imageView, - blobUrl, - }) satisfies ImageViewWithBlobUrl, - ), - ), - ); + private fetchImageViewWithBlobUrl(username: string, filename: string): Observable { + return this.httpClient + .get(this.endpointService.getUrl('images', [username, filename]), { + responseType: 'blob', + }) + .pipe(map((blob) => URL.createObjectURL(blob))); } - public getImage( - imageView: ImageView, - ): QueryOptions { - return queryOptions({ - queryKey: ['image-views-with-blob-urls', imageView.owner.username, imageView.filename], - queryFn: () => this.getImageViewWithBlobUrl(imageView), - }); + public getImageBlobUrl(username: string, filename: string): Observable { + const key = this.getUsernameFilenameKey(username, filename); + if (!this.imageBlobUrls.has(key)) { + const blobUrl$ = this.fetchImageViewWithBlobUrl(username, filename).pipe( + shareReplay({ bufferSize: 1, refCount: false }), + ); + this.imageBlobUrls.set(key, blobUrl$); + } + return this.imageBlobUrls.get(key)!; } public getImageView2(username: string, filename: string): Observable { @@ -144,15 +139,7 @@ export class ImageService { formData.append('isPublic', isPublic.toString()); } - return firstValueFrom( - this.httpClient.post(this.endpointService.getUrl('images'), formData).pipe( - tap(async () => { - await this.queryClient.invalidateQueries({ - queryKey: ['image-views'], - }); - }), - ), - ); + return firstValueFrom(this.httpClient.post(this.endpointService.getUrl('images'), formData)); } public imageExists(username: string, filename: string): Promise {