diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f2904ec0..268d5caadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + +### Added + +### Changed + +### Fixed + +## [3.0.0] - 2022-12-25 + ### Added - Editing: ability to edit posts and view edit history (on Rebased, Pleroma, and Mastodon). - Events: ability to create, view, and comment on Events (on Rebased). 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 dc34e73e01..71d1c9f296 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 481ae138e1..27b23a5039 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 { data: avatars, isFetching, isFetched, 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,104 @@ const FeedCarousel = () => { ); } - if (avatars.length === 0) { + if (isFetched && 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 7d9fa62f0f..9d2e1a3ecf 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 && ( -
+
)} ); diff --git a/package.json b/package.json index 4ef9a48c2b..9a16aeaf05 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "soapbox", "displayName": "Soapbox", - "version": "3.0.0", + "version": "3.1.0", "description": "Soapbox frontend for the Fediverse.", "homepage": "https://soapbox.pub/", "repository": { diff --git a/webpack/production.ts b/webpack/production.ts index a1af5a0f96..9ee4cf0890 100644 --- a/webpack/production.ts +++ b/webpack/production.ts @@ -131,6 +131,12 @@ const configuration: Configuration = { '/socket', '/static', '/unsubscribe', + '/images', + '/favicon.ico', + '/favicon.png', + '/apple-touch-icon.png', + '/browserconfig.xml', + '/robots.txt', ]; if (backendRoutes.some(path => pathname.startsWith(path)) || pathname.endsWith('/embed')) {