From b6afb8ae01f38d5ea2c9e72c5f5324d8d1de1c2f Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 22 Dec 2022 11:23:18 -0500 Subject: [PATCH 1/2] Add support for pinning an avatar in the Carousel --- .../features/feed-filtering/feed-carousel.tsx | 184 ++++++++++++------ .../components/placeholder-avatar.tsx | 4 +- 2 files changed, 122 insertions(+), 66 deletions(-) diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 481ae138e..23f417959 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -1,5 +1,5 @@ import classNames from 'clsx'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { replaceHomeTimeline } from 'soapbox/actions/timelines'; @@ -9,7 +9,10 @@ import { Avatar, useCarouselAvatars, useMarkAsSeen } from 'soapbox/queries/carou import { Card, HStack, Icon, Stack, Text } from '../../components/ui'; import PlaceholderAvatar from '../placeholder/components/placeholder-avatar'; -const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolean, onViewed: (account_id: string) => void }) => { +const CarouselItem = React.forwardRef(( + { avatar, seen, onViewed, onPinned }: { avatar: Avatar, seen: boolean, onViewed: (account_id: string) => void, onPinned?: (avatar: null | Avatar) => void }, + ref: any, +) => { const dispatch = useAppDispatch(); const markAsSeen = useMarkAsSeen(); @@ -28,7 +31,15 @@ const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolea if (isSelected) { dispatch(replaceHomeTimeline(null, { maxId: null }, () => setLoading(false))); + + if (onPinned) { + onPinned(null); + } } else { + if (onPinned) { + onPinned(avatar); + } + onViewed(avatar.account_id); markAsSeen.mutate(avatar.account_id); dispatch(replaceHomeTimeline(avatar.account_id, { maxId: null }, () => setLoading(false))); @@ -37,14 +48,15 @@ const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolea return (
- -
+ +
{isSelected && (
@@ -54,7 +66,7 @@ const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolea
); -}; +}); const FeedCarousel = () => { const { data: avatars, isFetching, isError } = useCarouselAvatars(); - const [cardRef, setCardRef, { width }] = useDimensions(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_ref, setContainerRef, { width }] = useDimensions(); const [seenAccountIds, setSeenAccountIds] = useState([]); const [pageSize, setPageSize] = useState(0); const [currentPage, setCurrentPage] = useState(1); + const [pinnedAvatar, setPinnedAvatar] = useState(null); + + const avatarsToList = useMemo(() => { + const list = avatars.filter((avatar) => avatar.account_id !== pinnedAvatar?.account_id); + if (pinnedAvatar) { + return [null, ...list]; + } + + return list; + }, [avatars, pinnedAvatar]); const numberOfPages = Math.ceil(avatars.length / pageSize); - const widthPerAvatar = (cardRef?.scrollWidth || 0) / avatars.length; + const widthPerAvatar = width / (Math.floor(width / 80)); const hasNextPage = currentPage < numberOfPages && numberOfPages > 1; const hasPrevPage = currentPage > 1 && numberOfPages > 1; @@ -118,67 +141,100 @@ const FeedCarousel = () => { ); } - if (avatars.length === 0) { - return null; - } - return ( - -
- {hasPrevPage && ( -
-
- -
-
- )} +
+ +
+ +
- - {isFetching ? ( - new Array(pageSize).fill(0).map((_, idx) => ( -
- -
- )) - ) : ( - avatars.map((avatar) => ( +
+ {pinnedAvatar ? ( +
setPinnedAvatar(avatar)} /> - )) - )} - - - {hasNextPage && ( -
-
-
-
- )} -
- + ) : null} + + + {isFetching ? ( + new Array(20).fill(0).map((_, idx) => ( +
+ +
+ )) + ) : ( + avatarsToList.map((avatar: any, index) => ( +
+ {avatar === null ? ( + +
+
+
+ + ) : ( + { + setPinnedAvatar(null); + setTimeout(() => { + setPinnedAvatar(avatar); + }, 1); + }} + onViewed={markAsSeen} + /> + )} +
+ )) + )} + +
+ +
+ +
+
+
); }; diff --git a/app/soapbox/features/placeholder/components/placeholder-avatar.tsx b/app/soapbox/features/placeholder/components/placeholder-avatar.tsx index 7d9fa62f0..9d2e1a3ec 100644 --- a/app/soapbox/features/placeholder/components/placeholder-avatar.tsx +++ b/app/soapbox/features/placeholder/components/placeholder-avatar.tsx @@ -21,14 +21,14 @@ const PlaceholderAvatar: React.FC = ({ size, withText = fals }, [size]); return ( - +
{withText && ( -
+
)} ); From 51146574d4a3c3c393b4c04a9c3cbfa8f47ee4f8 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 22 Dec 2022 12:02:17 -0500 Subject: [PATCH 2/2] Fix tests --- .../__tests__/feed-carousel.test.tsx | 31 +++---------------- .../features/feed-filtering/feed-carousel.tsx | 6 +++- 2 files changed, 10 insertions(+), 27 deletions(-) diff --git a/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx b/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx index dc34e73e0..71d1c9f29 100644 --- a/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx +++ b/app/soapbox/features/feed-filtering/__tests__/feed-carousel.test.tsx @@ -8,7 +8,7 @@ import { render, screen, waitFor } from '../../../jest/test-helpers'; import FeedCarousel from '../feed-carousel'; jest.mock('../../../hooks/useDimensions', () => ({ - useDimensions: () => [{ scrollWidth: 190 }, null, { width: 100 }], + useDimensions: () => [{ scrollWidth: 190 }, null, { width: 300 }], })); (window as any).ResizeObserver = class ResizeObserver { @@ -21,27 +21,6 @@ jest.mock('../../../hooks/useDimensions', () => ({ describe('', () => { let store: any; - describe('with "carousel" disabled', () => { - beforeEach(() => { - store = { - instance: { - version: '2.7.2 (compatible; Pleroma 2.4.52-1337-g4779199e.gleasonator+soapbox)', - pleroma: ImmutableMap({ - metadata: ImmutableMap({ - features: [], - }), - }), - }, - }; - }); - - it('should render nothing', () => { - render(, undefined, store); - - expect(screen.queryAllByTestId('feed-carousel')).toHaveLength(0); - }); - }); - describe('with "carousel" enabled', () => { beforeEach(() => { store = { @@ -167,15 +146,15 @@ describe('', () => { render(, undefined, store); await waitFor(() => { - expect(screen.getByTestId('next-page')).toBeInTheDocument(); - expect(screen.queryAllByTestId('prev-page')).toHaveLength(0); + expect(screen.getByTestId('prev-page')).toHaveAttribute('disabled'); + expect(screen.getByTestId('next-page')).not.toHaveAttribute('disabled'); }); await user.click(screen.getByTestId('next-page')); await waitFor(() => { - expect(screen.getByTestId('prev-page')).toBeInTheDocument(); - expect(screen.queryAllByTestId('next-page')).toHaveLength(0); + expect(screen.getByTestId('prev-page')).not.toHaveAttribute('disabled'); + expect(screen.getByTestId('next-page')).toHaveAttribute('disabled'); }); }); }); diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 23f417959..27b23a503 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -83,7 +83,7 @@ const CarouselItem = React.forwardRef(( }); const FeedCarousel = () => { - const { data: avatars, isFetching, isError } = useCarouselAvatars(); + const { data: avatars, isFetching, isFetched, isError } = useCarouselAvatars(); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [_ref, setContainerRef, { width }] = useDimensions(); @@ -141,6 +141,10 @@ const FeedCarousel = () => { ); } + if (isFetched && avatars.length === 0) { + return null; + } + return (