Remove Truth Social-specific features
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@@ -27,7 +27,7 @@ const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDiv
|
||||
}}
|
||||
data-testid='group-grid-item'
|
||||
>
|
||||
<Link to={`/group/${group.slug}`}>
|
||||
<Link to={`/group/${group.id}`}>
|
||||
<Stack
|
||||
className='aspect-h-7 aspect-w-10 h-full w-full overflow-hidden rounded-lg'
|
||||
ref={ref}
|
||||
|
||||
@@ -22,7 +22,7 @@ const GroupListItem = (props: IGroupListItem) => {
|
||||
justifyContent='between'
|
||||
data-testid='group-list-item'
|
||||
>
|
||||
<Link key={group.id} to={`/group/${group.slug}`} className='overflow-hidden'>
|
||||
<Link key={group.id} to={`/group/${group.id}`} className='overflow-hidden'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<GroupAvatar
|
||||
group={group}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { usePopularGroups } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
|
||||
|
||||
import GroupGridItem from './group-grid-item';
|
||||
|
||||
const PopularGroups = () => {
|
||||
const { groups, isFetching, isFetched, isError } = usePopularGroups();
|
||||
const isEmpty = (isFetched && groups.length === 0) || isError;
|
||||
|
||||
const [groupCover, setGroupCover] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<Stack space={4} data-testid='popular-groups'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.popular.title'
|
||||
defaultMessage='Popular Groups'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Link to='/groups/popular'>
|
||||
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.popular.show_more'
|
||||
defaultMessage='Show More'
|
||||
/>
|
||||
</Text>
|
||||
</Link>
|
||||
</HStack>
|
||||
|
||||
{isEmpty ? (
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.popular.empty'
|
||||
defaultMessage='Unable to fetch popular groups at this time. Please check back later.'
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Carousel
|
||||
itemWidth={250}
|
||||
itemCount={groups.length}
|
||||
controlsHeight={groupCover?.clientHeight}
|
||||
isDisabled={isFetching}
|
||||
>
|
||||
{({ width }: { width: number }) => (
|
||||
<>
|
||||
{isFetching ? (
|
||||
new Array(4).fill(0).map((_, idx) => (
|
||||
<div
|
||||
className='relative flex shrink-0 flex-col space-y-2 px-1'
|
||||
style={{ width: width || 'auto' }}
|
||||
key={idx}
|
||||
>
|
||||
<PlaceholderGroupDiscover />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<GroupGridItem
|
||||
key={group.id}
|
||||
group={group}
|
||||
width={width}
|
||||
ref={setGroupCover}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Carousel>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopularGroups;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { usePopularTags } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import TagListItem from './tag-list-item';
|
||||
|
||||
const PopularTags = () => {
|
||||
const { tags, isFetched, isError } = usePopularTags();
|
||||
const isEmpty = (isFetched && tags.length === 0) || isError;
|
||||
|
||||
return (
|
||||
<Stack space={4} data-testid='popular-tags'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.tags.title'
|
||||
defaultMessage='Browse Topics'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Link to='/groups/tags'>
|
||||
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.tags.show_more'
|
||||
defaultMessage='Show More'
|
||||
/>
|
||||
</Text>
|
||||
</Link>
|
||||
</HStack>
|
||||
|
||||
{isEmpty ? (
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.tags.empty'
|
||||
defaultMessage='Unable to fetch popular topics at this time. Please check back later.'
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Stack space={4}>
|
||||
{tags.slice(0, 10).map((tag) => (
|
||||
<TagListItem key={tag.id} tag={tag} />
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopularTags;
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import Blankslate from './blankslate';
|
||||
|
||||
|
||||
describe('<Blankslate />', () => {
|
||||
describe('with string props', () => {
|
||||
it('should render correctly', () => {
|
||||
render(<Blankslate title='Title' subtitle='Subtitle' />);
|
||||
|
||||
expect(screen.getByTestId('no-results')).toHaveTextContent('Title');
|
||||
expect(screen.getByTestId('no-results')).toHaveTextContent('Subtitle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with node props', () => {
|
||||
it('should render correctly', () => {
|
||||
render(
|
||||
<Blankslate
|
||||
title={<span>Title</span>}
|
||||
subtitle={<span>Subtitle</span>}
|
||||
/>);
|
||||
|
||||
expect(screen.getByTestId('no-results')).toHaveTextContent('Title');
|
||||
expect(screen.getByTestId('no-results')).toHaveTextContent('Subtitle');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface Props {
|
||||
title: React.ReactNode | string;
|
||||
subtitle: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export default ({ title, subtitle }: Props) => (
|
||||
<Stack space={2} className='px-4 py-2' data-testid='no-results'>
|
||||
<Text weight='bold' size='lg'>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import { groupSearchHistory } from 'soapbox/settings';
|
||||
import { clearRecentGroupSearches } from 'soapbox/utils/groups';
|
||||
|
||||
interface Props {
|
||||
onSelect(value: string): void;
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const { onSelect } = props;
|
||||
|
||||
const { account: me } = useOwnAccount();
|
||||
|
||||
const [recentSearches, setRecentSearches] = useState<string[]>(groupSearchHistory.get(me?.id as string) || []);
|
||||
|
||||
const onClearRecentSearches = () => {
|
||||
clearRecentGroupSearches(me?.id as string);
|
||||
setRecentSearches([]);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack space={2} data-testid='recent-searches'>
|
||||
{recentSearches.length > 0 ? (
|
||||
<>
|
||||
<HStack
|
||||
alignItems='center'
|
||||
justifyContent='between'
|
||||
className='bg-white dark:bg-gray-900'
|
||||
>
|
||||
<Text theme='muted' weight='semibold' size='sm'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.recent_searches.title'
|
||||
defaultMessage='Recent searches'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<button onClick={onClearRecentSearches} data-testid='clear-recent-searches'>
|
||||
<Text theme='primary' size='sm' className='hover:underline'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.recent_searches.clear_all'
|
||||
defaultMessage='Clear all'
|
||||
/>
|
||||
</Text>
|
||||
</button>
|
||||
</HStack>
|
||||
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={recentSearches}
|
||||
itemContent={(_index, recentSearch) => (
|
||||
<div key={recentSearch} data-testid='recent-search'>
|
||||
<button
|
||||
onClick={() => onSelect(recentSearch)}
|
||||
className='group flex w-full flex-col rounded-lg p-2 hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
data-testid='recent-search-result'
|
||||
>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className='flex h-10 w-10 items-center justify-center rounded-full bg-gray-200 p-2 dark:bg-gray-800 dark:group-hover:bg-gray-700/20'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/search.svg')}
|
||||
className='h-5 w-5 text-gray-600'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text weight='bold' size='sm' align='left'>{recentSearch}</Text>
|
||||
</HStack>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Stack space={2} data-testid='recent-searches-blankslate'>
|
||||
<Text weight='bold' size='lg'>
|
||||
<FormattedMessage id='groups.discover.search.recent_searches.blankslate.title' defaultMessage='No recent searches' />
|
||||
</Text>
|
||||
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='groups.discover.search.recent_searches.blankslate.subtitle' defaultMessage='Search group names, topics or keywords' />
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { VirtuosoGridMockContext, VirtuosoMockContext } from 'react-virtuoso';
|
||||
|
||||
import { buildAccount, buildGroup } from 'soapbox/jest/factory';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import Results from './results';
|
||||
|
||||
const userId = '1';
|
||||
const store = {
|
||||
me: userId,
|
||||
accounts: {
|
||||
[userId]: buildAccount({
|
||||
id: userId,
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
source: {
|
||||
chats_onboarded: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const renderApp = (children: React.ReactNode) => (
|
||||
render(
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>
|
||||
<VirtuosoGridMockContext.Provider value={{ viewportHeight: 300, viewportWidth: 300, itemHeight: 100, itemWidth: 100 }}>
|
||||
{children}
|
||||
</VirtuosoGridMockContext.Provider>
|
||||
</VirtuosoMockContext.Provider>,
|
||||
undefined,
|
||||
store,
|
||||
)
|
||||
);
|
||||
|
||||
const groupSearchResult = {
|
||||
groups: [buildGroup()],
|
||||
hasNextPage: false,
|
||||
isFetching: false,
|
||||
fetchNextPage: vi.fn(),
|
||||
} as any;
|
||||
|
||||
describe('<Results />', () => {
|
||||
describe('with a list layout', () => {
|
||||
it('should render the GroupListItem components', async () => {
|
||||
renderApp(<Results groupSearchResult={groupSearchResult} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-list-item')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a grid layout', () => {
|
||||
it('should render the GroupGridItem components', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderApp(<Results groupSearchResult={groupSearchResult} />);
|
||||
|
||||
await user.click(screen.getByTestId('layout-grid-action'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-grid-item')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,92 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useGroupSearch } from 'soapbox/api/hooks';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import GroupGridItem from '../group-grid-item';
|
||||
import GroupListItem from '../group-list-item';
|
||||
import LayoutButtons, { GroupLayout } from '../layout-buttons';
|
||||
|
||||
import type { Group } from 'soapbox/types/entities';
|
||||
|
||||
interface Props {
|
||||
groupSearchResult: ReturnType<typeof useGroupSearch>;
|
||||
}
|
||||
|
||||
const GridList: Components['List'] = React.forwardRef((props, ref) => {
|
||||
const { context, ...rest } = props;
|
||||
return <div ref={ref} {...rest} className='flex flex-wrap' />;
|
||||
});
|
||||
|
||||
export default (props: Props) => {
|
||||
const { groupSearchResult } = props;
|
||||
|
||||
const [layout, setLayout] = useState<GroupLayout>(GroupLayout.LIST);
|
||||
|
||||
const { groups, hasNextPage, isFetching, fetchNextPage } = groupSearchResult;
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage && !isFetching) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderGroupList = useCallback((group: Group, index: number) => (
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'pt-4': index !== 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GroupListItem group={group} withJoinAction />
|
||||
</div>
|
||||
), []);
|
||||
|
||||
const renderGroupGrid = useCallback((group: Group) => (
|
||||
<GroupGridItem group={group} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<Stack space={4} data-testid='results'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text weight='semibold'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.results.groups'
|
||||
defaultMessage='Groups'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<LayoutButtons
|
||||
layout={layout}
|
||||
onSelect={(selectedLayout) => setLayout(selectedLayout)}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{layout === GroupLayout.LIST ? (
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(index, group) => renderGroupList(group, index)}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<VirtuosoGrid
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(_index, group) => renderGroupGrid(group)}
|
||||
components={{
|
||||
Item: (props) => (
|
||||
<div {...props} className='w-1/2 flex-none pb-4 [&:nth-last-of-type(-n+2)]:pb-0' />
|
||||
),
|
||||
List: GridList,
|
||||
}}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,63 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { buildGroup } from 'soapbox/jest/factory';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { instanceSchema } from 'soapbox/schemas';
|
||||
|
||||
import Search from './search';
|
||||
|
||||
const store = {
|
||||
instance: instanceSchema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)',
|
||||
}),
|
||||
};
|
||||
|
||||
const renderApp = (children: React.ReactElement) => render(children, undefined, store);
|
||||
|
||||
describe('<Search />', () => {
|
||||
describe('with no results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups/search').reply(200, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the blankslate', async () => {
|
||||
renderApp(<Search searchValue={'some-search'} onSelect={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/groups/search').reply(200, [
|
||||
buildGroup({
|
||||
display_name: 'Group',
|
||||
id: '1',
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the results', async () => {
|
||||
renderApp(<Search searchValue={'some-search'} onSelect={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('before starting a search', () => {
|
||||
it('should render the RecentSearches component', () => {
|
||||
renderApp(<Search searchValue={''} onSelect={vi.fn()} />);
|
||||
|
||||
expect(screen.getByTestId('recent-searches')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,99 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useGroupSearch } from 'soapbox/api/hooks';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search';
|
||||
import { useDebounce, useOwnAccount } from 'soapbox/hooks';
|
||||
import { saveGroupSearch } from 'soapbox/utils/groups';
|
||||
|
||||
import Blankslate from './blankslate';
|
||||
import RecentSearches from './recent-searches';
|
||||
import Results from './results';
|
||||
|
||||
interface Props {
|
||||
onSelect(value: string): void;
|
||||
searchValue: string;
|
||||
}
|
||||
|
||||
export default (props: Props) => {
|
||||
const { onSelect, searchValue } = props;
|
||||
|
||||
const { account: me } = useOwnAccount();
|
||||
const debounce = useDebounce;
|
||||
|
||||
const debouncedValue = debounce(searchValue as string, 300);
|
||||
const debouncedValueToSave = debounce(searchValue as string, 1000);
|
||||
|
||||
const groupSearchResult = useGroupSearch(debouncedValue);
|
||||
const { groups, isLoading, isFetched, isError } = groupSearchResult;
|
||||
|
||||
const hasSearchResults = isFetched && groups.length > 0;
|
||||
const hasNoSearchResults = isFetched && groups.length === 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (debouncedValueToSave && debouncedValueToSave.length >= 0) {
|
||||
saveGroupSearch(me?.id as string, debouncedValueToSave);
|
||||
}
|
||||
}, [debouncedValueToSave]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Stack space={4}>
|
||||
<PlaceholderGroupSearch />
|
||||
<PlaceholderGroupSearch />
|
||||
<PlaceholderGroupSearch />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<Blankslate
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.error.title'
|
||||
defaultMessage='An error occurred'
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.error.subtitle'
|
||||
defaultMessage='Please try again later.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasNoSearchResults) {
|
||||
return (
|
||||
<Blankslate
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.no_results.title'
|
||||
defaultMessage='No matches found'
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<FormattedMessage
|
||||
id='groups.discover.search.no_results.subtitle'
|
||||
defaultMessage='Try searching for another group.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasSearchResults) {
|
||||
return (
|
||||
<Results
|
||||
groupSearchResult={groupSearchResult}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RecentSearches onSelect={onSelect} />
|
||||
);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useSuggestedGroups } from 'soapbox/api/hooks';
|
||||
import Link from 'soapbox/components/link';
|
||||
import { Carousel, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
|
||||
|
||||
import GroupGridItem from './group-grid-item';
|
||||
|
||||
const SuggestedGroups = () => {
|
||||
const { groups, isFetching, isFetched, isError } = useSuggestedGroups();
|
||||
const isEmpty = (isFetched && groups.length === 0) || isError;
|
||||
|
||||
const [groupCover, setGroupCover] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
return (
|
||||
<Stack space={4} data-testid='suggested-groups'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.suggested.title'
|
||||
defaultMessage='Suggested For You'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Link to='/groups/suggested'>
|
||||
<Text tag='span' weight='medium' size='sm' theme='inherit'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.suggested.show_more'
|
||||
defaultMessage='Show More'
|
||||
/>
|
||||
</Text>
|
||||
</Link>
|
||||
</HStack>
|
||||
|
||||
{isEmpty ? (
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.suggested.empty'
|
||||
defaultMessage='Unable to fetch suggested groups at this time. Please check back later.'
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Carousel
|
||||
itemWidth={250}
|
||||
itemCount={groups.length}
|
||||
controlsHeight={groupCover?.clientHeight}
|
||||
isDisabled={isFetching}
|
||||
>
|
||||
{({ width }: { width: number }) => (
|
||||
<>
|
||||
{isFetching ? (
|
||||
new Array(20).fill(0).map((_, idx) => (
|
||||
<div
|
||||
className='relative flex shrink-0 flex-col space-y-2 px-0.5'
|
||||
style={{ width: width || 'auto' }}
|
||||
key={idx}
|
||||
>
|
||||
<PlaceholderGroupDiscover />
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
groups.map((group) => (
|
||||
<GroupGridItem
|
||||
key={group.id}
|
||||
group={group}
|
||||
width={width}
|
||||
ref={setGroupCover}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Carousel>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SuggestedGroups;
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { buildGroupTag } from 'soapbox/jest/factory';
|
||||
import { render, screen } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import TagListItem from './tag-list-item';
|
||||
|
||||
describe('<TagListItem', () => {
|
||||
it('should render correctly', () => {
|
||||
const tag = buildGroupTag({ name: 'tag 1', groups: 5 });
|
||||
render(<TagListItem tag={tag} />);
|
||||
|
||||
expect(screen.getByTestId('tag-list-item')).toHaveTextContent(tag.name);
|
||||
expect(screen.getByTestId('tag-list-item')).toHaveTextContent('Number of groups: 5');
|
||||
});
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import type { GroupTag } from 'soapbox/schemas';
|
||||
|
||||
interface ITagListItem {
|
||||
tag: GroupTag;
|
||||
}
|
||||
|
||||
const TagListItem = (props: ITagListItem) => {
|
||||
const { tag } = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={`/groups/discover/tags/${tag.id}`}
|
||||
className='group'
|
||||
data-testid='tag-list-item'
|
||||
>
|
||||
<Stack>
|
||||
<Text
|
||||
weight='bold'
|
||||
className='group-hover:text-primary-600 group-hover:underline dark:group-hover:text-accent-blue'
|
||||
>
|
||||
#{tag.name}
|
||||
</Text>
|
||||
|
||||
<Text size='sm' theme='muted' weight='medium'>
|
||||
<FormattedMessage
|
||||
id='groups.discovery.tags.no_of_groups'
|
||||
defaultMessage='Number of groups'
|
||||
/>
|
||||
:{' '}
|
||||
{tag.groups}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagListItem;
|
||||
@@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui';
|
||||
import { type Card as StatusCard } from 'soapbox/types/entities';
|
||||
|
||||
interface IGroupLinkPreview {
|
||||
card: StatusCard;
|
||||
}
|
||||
|
||||
const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
|
||||
const { group } = card;
|
||||
if (!group) return null;
|
||||
|
||||
return (
|
||||
<Stack className='cursor-default overflow-hidden rounded-lg border border-gray-300 text-center dark:border-gray-800'>
|
||||
<div
|
||||
className='-mb-8 h-32 w-full bg-cover bg-center'
|
||||
style={{ backgroundImage: `url(${group.header})` }}
|
||||
/>
|
||||
|
||||
<Avatar
|
||||
className='mx-auto border-4 border-white dark:border-primary-900'
|
||||
src={group.avatar}
|
||||
size={64}
|
||||
/>
|
||||
|
||||
<Stack space={4} className='p-4'>
|
||||
<CardTitle title={<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />} />
|
||||
|
||||
<Button theme='primary' to={`/group/${group.slug}`} block>
|
||||
<FormattedMessage id='group.popover.action' defaultMessage='View Group' />
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export { GroupLinkPreview };
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { usePendingGroups } from 'soapbox/api/hooks';
|
||||
import { PendingItemsRow } from 'soapbox/components/pending-items-row';
|
||||
import { Divider } from 'soapbox/components/ui';
|
||||
import { useFeatures } from 'soapbox/hooks';
|
||||
|
||||
export default () => {
|
||||
const features = useFeatures();
|
||||
|
||||
const { groups, isFetching } = usePendingGroups();
|
||||
|
||||
if (!features.groupsPending || isFetching || groups.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<PendingItemsRow
|
||||
to='/groups/pending-requests'
|
||||
count={groups.length}
|
||||
size='lg'
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
|
||||
import { buildAccount } from 'soapbox/jest/factory';
|
||||
import { render, screen, waitFor } from 'soapbox/jest/test-helpers';
|
||||
import { instanceSchema } from 'soapbox/schemas';
|
||||
|
||||
import Discover from './discover';
|
||||
|
||||
vi.mock('../../../hooks/useDimensions', () => ({
|
||||
useDimensions: () => [{ scrollWidth: 190 }, null, { width: 300 }],
|
||||
}));
|
||||
|
||||
(window as any).ResizeObserver = class ResizeObserver {
|
||||
|
||||
observe() { }
|
||||
disconnect() { }
|
||||
|
||||
};
|
||||
|
||||
const userId = '1';
|
||||
const store: any = {
|
||||
me: userId,
|
||||
accounts: {
|
||||
[userId]: buildAccount({
|
||||
id: userId,
|
||||
acct: 'justin-username',
|
||||
display_name: 'Justin L',
|
||||
avatar: 'test.jpg',
|
||||
source: {
|
||||
chats_onboarded: false,
|
||||
},
|
||||
}),
|
||||
},
|
||||
instance: instanceSchema.parse({
|
||||
version: '3.4.1 (compatible; TruthSocial 1.0.0)',
|
||||
software: 'TRUTHSOCIAL',
|
||||
}),
|
||||
};
|
||||
|
||||
const renderApp = () => (
|
||||
render(
|
||||
<Discover />,
|
||||
undefined,
|
||||
store,
|
||||
)
|
||||
);
|
||||
|
||||
describe('<Discover />', () => {
|
||||
describe('before the user starts searching', () => {
|
||||
it('it should render popular groups', async () => {
|
||||
renderApp();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('popular-groups')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('suggested-groups')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('popular-tags')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('recent-searches')).toHaveLength(0);
|
||||
expect(screen.queryAllByTestId('group-search-icon')).toHaveLength(0);
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user focuses on the input', () => {
|
||||
it('should render the search experience', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderApp();
|
||||
|
||||
await user.click(screen.getByTestId('search'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('group-search-icon')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('recent-searches')).toBeInTheDocument();
|
||||
expect(screen.queryAllByTestId('popular-groups')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { HStack, Icon, IconButton, Input, Stack } from 'soapbox/components/ui';
|
||||
|
||||
import PopularGroups from './components/discover/popular-groups';
|
||||
import PopularTags from './components/discover/popular-tags';
|
||||
import Search from './components/discover/search/search';
|
||||
import SuggestedGroups from './components/discover/suggested-groups';
|
||||
import TabBar, { TabItems } from './components/tab-bar';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'groups.discover.search.placeholder', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
const Discover: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [isSearching, setIsSearching] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>('');
|
||||
|
||||
const hasSearchValue = value && value.length > 0;
|
||||
|
||||
const cancelSearch = () => {
|
||||
clearValue();
|
||||
setIsSearching(false);
|
||||
};
|
||||
|
||||
const clearValue = () => setValue('');
|
||||
|
||||
return (
|
||||
<Stack space={4}>
|
||||
<TabBar activeTab={TabItems.FIND_GROUPS} />
|
||||
|
||||
<Stack space={6}>
|
||||
<HStack alignItems='center'>
|
||||
{isSearching ? (
|
||||
<IconButton
|
||||
src={require('@tabler/icons/outline/arrow-left.svg')}
|
||||
iconClassName='mr-2 h-5 w-5 fill-current text-gray-600 rtl:rotate-180'
|
||||
onClick={cancelSearch}
|
||||
data-testid='group-search-icon'
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Input
|
||||
data-testid='search'
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onFocus={() => setIsSearching(true)}
|
||||
outerClassName='mt-0 w-full'
|
||||
theme='search'
|
||||
append={
|
||||
<button onClick={clearValue}>
|
||||
<Icon
|
||||
src={hasSearchValue ? require('@tabler/icons/outline/x.svg') : require('@tabler/icons/outline/search.svg')}
|
||||
className='h-4 w-4 text-gray-700 dark:text-gray-600'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</HStack>
|
||||
|
||||
{isSearching ? (
|
||||
<Search
|
||||
searchValue={value}
|
||||
onSelect={(newValue) => setValue(newValue)}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<PopularGroups />
|
||||
<SuggestedGroups />
|
||||
<PopularTags />
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default Discover;
|
||||
@@ -1,36 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { useGroups } from 'soapbox/api/hooks';
|
||||
import GroupCard from 'soapbox/components/group-card';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Input, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useDebounce, useFeatures } from 'soapbox/hooks';
|
||||
import { Button, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { PERMISSION_CREATE_GROUPS, hasPermission } from 'soapbox/utils/permissions';
|
||||
|
||||
import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
|
||||
|
||||
import PendingGroupsRow from './components/pending-groups-row';
|
||||
import TabBar, { TabItems } from './components/tab-bar';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'groups.search.placeholder', defaultMessage: 'Search My Groups' },
|
||||
});
|
||||
|
||||
const Groups: React.FC = () => {
|
||||
const debounce = useDebounce;
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
||||
const canCreateGroup = useAppSelector((state) => hasPermission(state, PERMISSION_CREATE_GROUPS));
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const debouncedValue = debounce(searchValue, 300);
|
||||
|
||||
const { groups, isLoading, hasNextPage, fetchNextPage } = useGroups(debouncedValue);
|
||||
const { groups, isLoading, hasNextPage, fetchNextPage } = useGroups();
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage) {
|
||||
@@ -72,10 +59,6 @@ const Groups: React.FC = () => {
|
||||
|
||||
return (
|
||||
<Stack space={4}>
|
||||
{features.groupsDiscovery && (
|
||||
<TabBar activeTab={TabItems.MY_GROUPS} />
|
||||
)}
|
||||
|
||||
{canCreateGroup && (
|
||||
<Button
|
||||
className='xl:hidden'
|
||||
@@ -88,17 +71,6 @@ const Groups: React.FC = () => {
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{features.groupsSearch ? (
|
||||
<Input
|
||||
onChange={(event) => setSearchValue(event.target.value)}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
theme='search'
|
||||
value={searchValue}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<PendingGroupsRow />
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='groups'
|
||||
emptyMessage={renderBlankslate()}
|
||||
@@ -112,7 +84,7 @@ const Groups: React.FC = () => {
|
||||
hasMore={hasNextPage}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<Link key={group.id} to={`/group/${group.slug}`}>
|
||||
<Link key={group.id} to={`/group/${group.id}`}>
|
||||
<GroupCard group={group} />
|
||||
</Link>
|
||||
))}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { usePendingGroups } from 'soapbox/api/hooks';
|
||||
import GroupCard from 'soapbox/components/group-card';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'groups.pending.label', defaultMessage: 'Pending Requests' },
|
||||
});
|
||||
|
||||
export default () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { groups, isLoading } = usePendingGroups();
|
||||
|
||||
const renderBlankslate = () => (
|
||||
<Stack
|
||||
space={4}
|
||||
alignItems='center'
|
||||
justifyContent='center'
|
||||
className='py-6'
|
||||
data-testid='pending-requests-blankslate'
|
||||
>
|
||||
<Stack space={2} className='max-w-sm'>
|
||||
<Text size='2xl' weight='bold' tag='h2' align='center'>
|
||||
<FormattedMessage
|
||||
id='groups.pending.empty.title'
|
||||
defaultMessage='No pending requests'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<Text size='sm' theme='muted' align='center'>
|
||||
<FormattedMessage
|
||||
id='groups.pending.empty.subtitle'
|
||||
defaultMessage='You have no pending requests at this time.'
|
||||
/>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.label)}>
|
||||
<ScrollableList
|
||||
emptyMessage={renderBlankslate()}
|
||||
emptyMessageCard={false}
|
||||
isLoading={isLoading}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
placeholderComponent={PlaceholderGroupCard}
|
||||
placeholderCount={3}
|
||||
scrollKey='pending-group-requests'
|
||||
showLoading={isLoading && groups.length === 0}
|
||||
>
|
||||
{groups.map((group) => (
|
||||
<Link key={group.id} to={`/group/${group.slug}`}>
|
||||
<GroupCard group={group} />
|
||||
</Link>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
@@ -1,88 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { usePopularGroups } from 'soapbox/api/hooks';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
import LayoutButtons, { GroupLayout } from './components/discover/layout-buttons';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'groups.popular.label', defaultMessage: 'Suggested Groups' },
|
||||
});
|
||||
|
||||
const GridList: Components['List'] = React.forwardRef((props, ref) => {
|
||||
const { context, ...rest } = props;
|
||||
return <div ref={ref} {...rest} className='flex flex-wrap' />;
|
||||
});
|
||||
|
||||
const Popular: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [layout, setLayout] = useState<GroupLayout>(GroupLayout.LIST);
|
||||
|
||||
const { groups, hasNextPage, fetchNextPage } = usePopularGroups();
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderGroupList = useCallback((group: Group, index: number) => (
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'pt-4': index !== 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GroupListItem group={group} withJoinAction />
|
||||
</div>
|
||||
), []);
|
||||
|
||||
const renderGroupGrid = useCallback((group: Group) => (
|
||||
<GroupGridItem group={group} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<Column
|
||||
label={intl.formatMessage(messages.label)}
|
||||
action={
|
||||
<LayoutButtons
|
||||
layout={layout}
|
||||
onSelect={(selectedLayout) => setLayout(selectedLayout)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{layout === GroupLayout.LIST ? (
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(index, group) => renderGroupList(group, index)}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<VirtuosoGrid
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(_index, group) => renderGroupGrid(group)}
|
||||
components={{
|
||||
Item: (props) => (
|
||||
<div {...props} className='w-1/2 flex-none pb-4 [&:nth-last-of-type(-n+2)]:pb-0' />
|
||||
),
|
||||
List: GridList,
|
||||
}}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Popular;
|
||||
@@ -1,88 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useSuggestedGroups } from 'soapbox/api/hooks';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
import LayoutButtons, { GroupLayout } from './components/discover/layout-buttons';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'groups.suggested.label', defaultMessage: 'Suggested Groups' },
|
||||
});
|
||||
|
||||
const GridList: Components['List'] = React.forwardRef((props, ref) => {
|
||||
const { context, ...rest } = props;
|
||||
return <div ref={ref} {...rest} className='flex flex-wrap' />;
|
||||
});
|
||||
|
||||
const Suggested: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [layout, setLayout] = useState<GroupLayout>(GroupLayout.LIST);
|
||||
|
||||
const { groups, hasNextPage, fetchNextPage } = useSuggestedGroups();
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderGroupList = useCallback((group: Group, index: number) => (
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'pt-4': index !== 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GroupListItem group={group} withJoinAction />
|
||||
</div>
|
||||
), []);
|
||||
|
||||
const renderGroupGrid = useCallback((group: Group) => (
|
||||
<GroupGridItem group={group} />
|
||||
), []);
|
||||
|
||||
return (
|
||||
<Column
|
||||
label={intl.formatMessage(messages.label)}
|
||||
action={
|
||||
<LayoutButtons
|
||||
layout={layout}
|
||||
onSelect={(selectedLayout) => setLayout(selectedLayout)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{layout === GroupLayout.LIST ? (
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(index, group) => renderGroupList(group, index)}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<VirtuosoGrid
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(_index, group) => renderGroupGrid(group)}
|
||||
components={{
|
||||
Item: (props) => (
|
||||
<div {...props} className='w-1/2 flex-none pb-4 [&:nth-last-of-type(-n+2)]:pb-0' />
|
||||
),
|
||||
List: GridList,
|
||||
}}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Suggested;
|
||||
@@ -1,115 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
|
||||
|
||||
import { useGroupTag, useGroupsFromTag } from 'soapbox/api/hooks';
|
||||
import { Column, HStack, Icon } from 'soapbox/components/ui';
|
||||
|
||||
import GroupGridItem from './components/discover/group-grid-item';
|
||||
import GroupListItem from './components/discover/group-list-item';
|
||||
|
||||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
enum Layout {
|
||||
LIST = 'LIST',
|
||||
GRID = 'GRID'
|
||||
}
|
||||
|
||||
const GridList: Components['List'] = React.forwardRef((props, ref) => {
|
||||
const { context, ...rest } = props;
|
||||
return <div ref={ref} {...rest} className='flex flex-wrap' />;
|
||||
});
|
||||
|
||||
interface ITag {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
const Tag: React.FC<ITag> = (props) => {
|
||||
const tagId = props.params.id;
|
||||
|
||||
const [layout, setLayout] = useState<Layout>(Layout.LIST);
|
||||
|
||||
const { tag, isLoading } = useGroupTag(tagId);
|
||||
const { groups, hasNextPage, fetchNextPage } = useGroupsFromTag(tagId);
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderGroupList = useCallback((group: Group, index: number) => (
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'pt-4': index !== 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<GroupListItem group={group} withJoinAction />
|
||||
</div>
|
||||
), []);
|
||||
|
||||
const renderGroupGrid = useCallback((group: Group) => (
|
||||
<GroupGridItem group={group} />
|
||||
), []);
|
||||
|
||||
if (isLoading || !tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Column
|
||||
label={`#${tag.name}`}
|
||||
action={
|
||||
<HStack alignItems='center'>
|
||||
<button onClick={() => setLayout(Layout.LIST)}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/layout-list.svg')}
|
||||
className={
|
||||
clsx('h-5 w-5 text-gray-600', {
|
||||
'text-primary-600': layout === Layout.LIST,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<button onClick={() => setLayout(Layout.GRID)}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/layout-grid.svg')}
|
||||
className={
|
||||
clsx('h-5 w-5 text-gray-600', {
|
||||
'text-primary-600': layout === Layout.GRID,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</HStack>
|
||||
}
|
||||
>
|
||||
{layout === Layout.LIST ? (
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(index, group) => renderGroupList(group, index)}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
) : (
|
||||
<VirtuosoGrid
|
||||
useWindowScroll
|
||||
data={groups}
|
||||
itemContent={(_index, group) => renderGroupGrid(group)}
|
||||
components={{
|
||||
Item: (props) => (
|
||||
<div {...props} className='w-1/2 flex-none pb-4 [&:nth-last-of-type(-n+2)]:pb-0' />
|
||||
),
|
||||
List: GridList,
|
||||
}}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tag;
|
||||
@@ -1,62 +0,0 @@
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { usePopularTags } from 'soapbox/api/hooks';
|
||||
import { Column, Text } from 'soapbox/components/ui';
|
||||
|
||||
import TagListItem from './components/discover/tag-list-item';
|
||||
|
||||
import type { GroupTag } from 'soapbox/schemas';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'groups.tags.title', defaultMessage: 'Browse Topics' },
|
||||
});
|
||||
|
||||
const Tags: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { tags, isFetched, isError, hasNextPage, fetchNextPage } = usePopularTags();
|
||||
const isEmpty = (isFetched && tags.length === 0) || isError;
|
||||
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (index: number, tag: GroupTag) => (
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'pt-4': index !== 0,
|
||||
})
|
||||
}
|
||||
>
|
||||
<TagListItem key={tag.id} tag={tag} />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
{isEmpty ? (
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage
|
||||
id='groups.discover.tags.empty'
|
||||
defaultMessage='Unable to fetch popular topics at this time. Please check back later.'
|
||||
/>
|
||||
</Text>
|
||||
) : (
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
data={tags}
|
||||
itemContent={renderItem}
|
||||
endReached={handleLoadMore}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tags;
|
||||
Reference in New Issue
Block a user