diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
index b24146c798..e97affb322 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/SingleSelectionMenuItems.tsx
@@ -36,7 +36,7 @@ import {
import { ImageDTO } from 'services/api/types';
import { useDebounce } from 'use-debounce';
import { sentImageToCanvas, sentImageToImg2Img } from '../../store/actions';
-import { BsPinAngle, BsPinAngleFill } from 'react-icons/bs';
+import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs';
type SingleSelectionMenuItemsProps = {
imageDTO: ImageDTO;
@@ -211,11 +211,14 @@ const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
Change Board
{imageDTO.pinned ? (
- } onClickCapture={handleUnpinImage}>
+ } onClickCapture={handleUnpinImage}>
Unpin Image
) : (
- } onClickCapture={handlePinImage}>
+ }
+ onClickCapture={handlePinImage}
+ >
Pin Image
)}
diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
index 97f8199aed..aba5181882 100644
--- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
+++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx
@@ -2,16 +2,19 @@ import { Box, Flex } from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import IAIDndImage from 'common/components/IAIDndImage';
import IAIFillSkeleton from 'common/components/IAIFillSkeleton';
-import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts';
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
-import { MouseEvent, memo, useCallback, useMemo } from 'react';
-import { FaTrash } from 'react-icons/fa';
-import { useGetImageDTOQuery } from 'services/api/endpoints/images';
import {
ImageDTOsDraggableData,
ImageDraggableData,
TypesafeDraggableData,
} from 'features/dnd/types';
+import { useMultiselect } from 'features/gallery/hooks/useMultiselect.ts';
+import { MouseEvent, memo, useCallback, useMemo } from 'react';
+import { BsBookmarkStar, BsFillBookmarkStarFill } from 'react-icons/bs';
+import {
+ useChangeImagePinnedMutation,
+ useGetImageDTOQuery,
+} from 'services/api/endpoints/images';
interface HoverableImageProps {
imageName: string;
@@ -59,6 +62,14 @@ const GalleryImage = (props: HoverableImageProps) => {
}
}, [imageDTO, selection, selectionCount]);
+ const [togglePin] = useChangeImagePinnedMutation();
+
+ const togglePinnedState = useCallback(() => {
+ if (imageDTO) {
+ togglePin({ imageDTO, pinned: !imageDTO.pinned });
+ }
+ }, [togglePin, imageDTO]);
+
if (!imageDTO) {
return ;
}
@@ -80,15 +91,17 @@ const GalleryImage = (props: HoverableImageProps) => {
draggableData={draggableData}
isSelected={isSelected}
minSize={0}
- onClickReset={handleDelete}
+ onClickReset={togglePinnedState}
imageSx={{ w: 'full', h: 'full' }}
isDropDisabled={true}
isUploadDisabled={true}
thumbnail={true}
withHoverOverlay
- resetIcon={}
- resetTooltip="Delete image"
- withResetIcon={shouldShowDeleteButton} // removed bc it's too easy to accidentally delete images
+ resetIcon={
+ imageDTO.pinned ? :
+ }
+ resetTooltip="Pin image"
+ withResetIcon={true}
/>
diff --git a/invokeai/frontend/web/src/services/api/endpoints/images.ts b/invokeai/frontend/web/src/services/api/endpoints/images.ts
index 9320e7a137..ef99d735c2 100644
--- a/invokeai/frontend/web/src/services/api/endpoints/images.ts
+++ b/invokeai/frontend/web/src/services/api/endpoints/images.ts
@@ -399,15 +399,18 @@ export const imagesApi = api.injectEndpoints({
method: 'PATCH',
body: { pinned },
}),
- invalidatesTags: (result, error, { imageDTO }) => [
- {
- type: 'ImageList',
- id: getListImagesUrl({
- board_id: imageDTO.board_id,
- categories: IMAGE_CATEGORIES,
- }),
- },
- ],
+ invalidatesTags: (result, error, { imageDTO }) => {
+ const categories = getCategories(imageDTO);
+ return [
+ {
+ type: 'ImageList',
+ id: getListImagesUrl({
+ board_id: imageDTO.board_id,
+ categories,
+ }),
+ },
+ ]
+ },
async onQueryStarted(
{ imageDTO, pinned },
{ dispatch, queryFulfilled, getState }
@@ -465,27 +468,14 @@ export const imagesApi = api.injectEndpoints({
const isCacheFullyPopulated =
currentCache.data && currentCache.data.ids.length >= (total ?? 0);
- const isInDateRangeForPinnedState = getIsImageInDateRange(
+ const isInDateRange = getIsImageInDateRange(
currentCache.data,
updatedImage
);
- if (!isInDateRangeForPinnedState) {
- // if newly pinned or unpinned image is not in date range for its new state, remove from cache
- patches.push(
- dispatch(
- imagesApi.util.updateQueryData(
- 'listImages',
- queryArgs,
- (draft) => {
- imagesAdapter.removeOne(draft, updatedImage.image_name);
- }
- )
- )
- );
- }
+ // should we remove images from cache if _not_ in date range? ie you are showing 100 of 101 pinned images and you unpin one. technically it should disappear from list.
- if (isCacheFullyPopulated || isInDateRangeForPinnedState) {
+ if (isCacheFullyPopulated || isInDateRange) {
// *upsert* to $cache
patches.push(
dispatch(
diff --git a/invokeai/frontend/web/src/services/api/util.ts b/invokeai/frontend/web/src/services/api/util.ts
index 10c4557eec..a97e96f312 100644
--- a/invokeai/frontend/web/src/services/api/util.ts
+++ b/invokeai/frontend/web/src/services/api/util.ts
@@ -21,21 +21,35 @@ export const getIsImageInDateRange = (
return true;
}
- const cacheImageDTOSForPinnedState = totalCachedImageDtos.filter((image) => image.pinned === imageDTO.pinned);
+ const cachedPinnedImages = [];
+ const cachedUnpinnedImages = [];
- if (cacheImageDTOSForPinnedState.length > 1) {
- // Images are sorted by `pinned` DESC and then `created_at` DESC
- // check if the image is newer than the oldest image in the cache for either the pinned group or unpinned group
+ for (let index = 0; index < totalCachedImageDtos.length; index++) {
+ const image = totalCachedImageDtos[index];
+ if (image?.pinned) cachedPinnedImages.push(image)
+ if (!image?.pinned) cachedUnpinnedImages.push(image)
+ }
+
+ const lastPinnedImage = cachedPinnedImages[cachedPinnedImages.length - 1];
+ const lastUnpinnedImage = cachedUnpinnedImages[cachedUnpinnedImages.length - 1];
+
+ if (!lastPinnedImage || !lastUnpinnedImage) {
+ // satisfy TS gods, we already confirmed the array has more than one image
+ return false;
+ }
+
+ if (imageDTO.pinned) {
+ // if pinning or already pinned, want to look in list of pinned images
const createdDate = new Date(imageDTO.created_at);
- const oldestImage = cacheImageDTOSForPinnedState[cacheImageDTOSForPinnedState.length - 1];
- if (!oldestImage) {
- // satisfy TS gods, we already confirmed the array has more than one image
- return false;
- }
- const oldestDate = new Date(oldestImage.created_at);
+ const oldestDate = new Date(lastPinnedImage.created_at);
+ return createdDate >= oldestDate;
+ } else {
+ // if unpinning or already unpinned, want to look in list of unpinned images
+ const createdDate = new Date(imageDTO.created_at);
+ const oldestDate = new Date(lastUnpinnedImage.created_at);
return createdDate >= oldestDate;
}
- return false;
+
};
export const getCategories = (imageDTO: ImageDTO) => {