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')) {