Remove Truth Social-specific features

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-04-28 14:50:23 +02:00
parent 5ba66f79ba
commit 0308aec65b
163 changed files with 312 additions and 5471 deletions

View File

@@ -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}

View File

@@ -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}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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');
});
});
});

View File

@@ -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>
);

View File

@@ -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>
);
};

View File

@@ -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();
});
});
});
});

View File

@@ -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>
);
};

View File

@@ -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();
});
});
});

View File

@@ -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} />
);
};

View File

@@ -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;

View File

@@ -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');
});
});

View File

@@ -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;

View File

@@ -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 };

View File

@@ -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 />
</>
);
};

View File

@@ -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);
});
});
});
});

View File

@@ -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;

View File

@@ -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>
))}

View File

@@ -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>
);
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;