Add Trending and Suggested Groups to discovery

develop^2
Chewbacca 2023-02-27 08:25:59 -05:00
rodzic 6413bed23f
commit 0414c36a1e
21 zmienionych plików z 655 dodań i 109 usunięć

Wyświetl plik

@ -17,43 +17,53 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
const intl = useIntl();
return (
<div className='overflow-hidden'>
<Stack className='rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900 sm:rounded-xl'>
<div className='relative -m-[1px] mb-0 h-[120px] rounded-t-lg bg-primary-100 dark:bg-gray-800 sm:rounded-t-xl'>
{group.header && <img className='h-full w-full rounded-t-lg object-cover sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
</div>
</div>
<Stack className='p-3 pt-9' alignItems='center' space={3}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
{group.relationship?.role === 'admin' ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
<span><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></span>
</HStack>
) : group.relationship?.role === 'moderator' && (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
<span><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></span>
</HStack>
)}
{group.locked ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
<span><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></span>
</HStack>
) : (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
<span><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></span>
</HStack>
)}
</HStack>
</Stack>
<Stack className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'>
{/* Group Cover Image */}
<Stack grow className='relative basis-1/2 rounded-t-lg bg-primary-100 dark:bg-gray-800'>
{group.header && (
<img
className='absolute inset-0 h-full w-full rounded-t-lg object-cover'
src={group.header} alt={intl.formatMessage(messages.groupHeader)}
/>
)}
</Stack>
</div>
{/* Group Avatar */}
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
</div>
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<Text size='lg' weight='bold' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
{group.relationship?.role === 'admin' ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/users.svg')} />
<Text theme='inherit'><FormattedMessage id='group.role.admin' defaultMessage='Admin' /></Text>
</HStack>
) : group.relationship?.role === 'moderator' && (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/gavel.svg')} />
<Text theme='inherit'><FormattedMessage id='group.role.moderator' defaultMessage='Moderator' /></Text>
</HStack>
)}
{group.locked ? (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/lock.svg')} />
<Text theme='inherit'><FormattedMessage id='group.privacy.locked' defaultMessage='Private' /></Text>
</HStack>
) : (
<HStack space={1} alignItems='center'>
<Icon className='h-4 w-4' src={require('@tabler/icons/world.svg')} />
<Text theme='inherit'><FormattedMessage id='group.privacy.public' defaultMessage='Public' /></Text>
</HStack>
)}
</HStack>
</Stack>
</Stack>
);
};

Wyświetl plik

@ -0,0 +1,109 @@
import React, { useEffect, useState } from 'react';
import { useDimensions } from 'soapbox/hooks';
import HStack from '../hstack/hstack';
import Icon from '../icon/icon';
interface ICarousel {
children: any
/** Optional height to force on controls */
controlsHeight?: number
/** How many items in the carousel */
itemCount: number
/** The minimum width per item */
itemWidth: number
}
/**
* Carousel
*/
const Carousel: React.FC<ICarousel> = (props): JSX.Element => {
const { children, controlsHeight, itemCount, itemWidth } = props;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_ref, setContainerRef, { width: containerWidth }] = useDimensions();
const [pageSize, setPageSize] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1);
const numberOfPages = Math.ceil(itemCount / pageSize);
const width = containerWidth / (Math.floor(containerWidth / itemWidth));
const hasNextPage = currentPage < numberOfPages && numberOfPages > 1;
const hasPrevPage = currentPage > 1 && numberOfPages > 1;
const handleNextPage = () => setCurrentPage((prevPage) => prevPage + 1);
const handlePrevPage = () => setCurrentPage((prevPage) => prevPage - 1);
const renderChildren = () => {
if (typeof children === 'function') {
return children({ width: width || 'auto' });
}
return children;
};
useEffect(() => {
if (containerWidth) {
setPageSize(Math.round(containerWidth / width));
}
}, [containerWidth, width]);
return (
<HStack alignItems='stretch'>
<div
className='z-10 flex w-5 items-center justify-center self-stretch rounded-l-xl bg-white dark:bg-primary-900'
style={{
height: controlsHeight || 'auto',
}}
>
<button
data-testid='prev-page'
onClick={handlePrevPage}
className='flex h-full w-7 items-center justify-center transition-opacity duration-500 disabled:opacity-25'
disabled={!hasPrevPage}
>
<Icon
src={require('@tabler/icons/chevron-left.svg')}
className='h-5 w-5 text-black dark:text-white'
/>
</button>
</div>
<div className='relative w-full overflow-hidden'>
<HStack
alignItems='center'
style={{
transform: `translateX(-${(currentPage - 1) * 100}%)`,
}}
className='transition-all duration-500 ease-out'
ref={setContainerRef}
>
{renderChildren()}
</HStack>
</div>
<div
className='z-10 flex w-5 items-center justify-center self-stretch rounded-r-xl bg-white dark:bg-primary-900'
style={{
height: controlsHeight || 'auto',
}}
>
<button
data-testid='next-page'
onClick={handleNextPage}
className='flex h-full w-7 items-center justify-center transition-opacity duration-500 disabled:opacity-25'
disabled={!hasNextPage}
>
<Icon
src={require('@tabler/icons/chevron-right.svg')}
className='h-5 w-5 text-black dark:text-white'
/>
</button>
</div>
</HStack>
);
};
export default Carousel;

Wyświetl plik

@ -2,6 +2,7 @@ export { default as Accordion } from './accordion/accordion';
export { default as Avatar } from './avatar/avatar';
export { default as Banner } from './banner/banner';
export { default as Button } from './button/button';
export { default as Carousel } from './carousel/carousel';
export { Card, CardBody, CardHeader, CardTitle } from './card/card';
export { default as Checkbox } from './checkbox/checkbox';
export { Column, ColumnHeader } from './column/column';

Wyświetl plik

@ -0,0 +1,93 @@
import React, { forwardRef } from 'react';
import { FormattedMessage } from 'react-intl';
import { Avatar, Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui';
import { Group as GroupEntity } from 'soapbox/types/entities';
import { shortNumberFormat } from 'soapbox/utils/numbers';
interface IGroup {
group: GroupEntity
width: number
}
const Group = forwardRef((props: IGroup, ref: React.ForwardedRef<HTMLDivElement>) => {
const { group, width = 'auto' } = props;
return (
<div
key={group.id}
className='relative flex shrink-0 flex-col space-y-2 px-0.5'
style={{
width,
}}
>
<Stack
className='aspect-w-10 aspect-h-7 h-full w-full overflow-hidden rounded-lg'
ref={ref}
style={{ minHeight: 180 }}
>
{group.header && (
<img
src={group.header}
alt='Group cover'
className='absolute inset-0 object-cover'
/>
)}
<Stack justifyContent='end' className='z-10 p-4 text-white' space={3}>
<Avatar
className='ring-2 ring-white'
src={group.avatar}
size={44}
/>
<Stack space={1}>
<Text
weight='bold'
dangerouslySetInnerHTML={{ __html: group.display_name_html }}
theme='inherit'
truncate
/>
<HStack space={1} alignItems='center'>
<Icon
className='h-4.5 w-4.5'
src={group.locked ? require('@tabler/icons/lock.svg') : require('@tabler/icons/world.svg')}
/>
{typeof group.members_count === 'undefined' ? (
<Text theme='inherit' tag='span' size='sm'>
{group.locked ? (
<FormattedMessage id='group.privacy.locked' defaultMessage='Private' />
) : (
<FormattedMessage id='group.privacy.public' defaultMessage='Public' />
)}
</Text>
) : (
<Text theme='inherit' tag='span' size='sm'>
{shortNumberFormat(group.members_count)}
{' '}
members
</Text>
)}
</HStack>
</Stack>
</Stack>
<div
className='absolute inset-x-0 bottom-0 z-0 flex justify-center rounded-b-lg bg-gradient-to-t from-gray-900 to-transparent pt-12 pb-8 transition-opacity duration-500'
/>
</Stack>
<Button
theme='primary'
block
>
Join Group
</Button>
</div>
);
});
export default Group;

Wyświetl plik

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Carousel, Stack, Text } from 'soapbox/components/ui';
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
import { usePopularGroups } from 'soapbox/queries/groups';
import Group from './group';
const PopularGroups = () => {
const { groups, isFetching } = usePopularGroups();
const [groupCover, setGroupCover] = useState<HTMLDivElement | null>(null);
return (
<Stack space={4}>
<Text size='xl' weight='bold'>
Popular Groups
</Text>
<Carousel
itemWidth={250}
itemCount={groups.length}
controlsHeight={groupCover?.clientHeight}
>
{({ 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) => (
<Group
key={group.id}
group={group}
width={width}
ref={setGroupCover}
/>
))
)}
</>
)}
</Carousel>
</Stack>
);
};
export default PopularGroups;

Wyświetl plik

@ -0,0 +1,54 @@
import React, { useState } from 'react';
import { Carousel, Stack, Text } from 'soapbox/components/ui';
import PlaceholderGroupDiscover from 'soapbox/features/placeholder/components/placeholder-group-discover';
import { useSuggestedGroups } from 'soapbox/queries/groups';
import Group from './group';
const SuggestedGroups = () => {
const { groups, isFetching } = useSuggestedGroups();
const [groupCover, setGroupCover] = useState<HTMLDivElement | null>(null);
return (
<Stack space={4}>
<Text size='xl' weight='bold'>
Suggested For You
</Text>
<Carousel
itemWidth={250}
itemCount={groups.length}
controlsHeight={groupCover?.clientHeight}
>
{({ 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) => (
<Group
key={group.id}
group={group}
width={width}
ref={setGroupCover}
/>
))
)}
</>
)}
</Carousel>
</Stack>
);
};
export default SuggestedGroups;

Wyświetl plik

@ -0,0 +1,41 @@
import React, { useMemo } from 'react';
import { useHistory } from 'react-router-dom';
import { Tabs } from 'soapbox/components/ui';
import type { Item } from 'soapbox/components/ui/tabs/tabs';
export enum TabItems {
MY_GROUPS = 'MY_GROUPS',
FIND_GROUPS = 'FIND_GROUPS'
}
interface ITabBar {
activeTab: TabItems
}
const TabBar = ({ activeTab }: ITabBar) => {
const history = useHistory();
const tabItems: Item[] = useMemo(() => ([
{
text: 'My Groups',
action: () => history.push('/groups'),
name: TabItems.MY_GROUPS,
},
{
text: 'Find Groups',
action: () => history.push('/groups/discover'),
name: TabItems.FIND_GROUPS,
},
]), []);
return (
<Tabs
items={tabItems}
activeItem={activeTab}
/>
);
};
export default TabBar;

Wyświetl plik

@ -0,0 +1,22 @@
import React from 'react';
import { Stack } from 'soapbox/components/ui';
import PopularGroups from './components/discover/popular-groups';
import SuggestedGroups from './components/discover/suggested-groups';
import TabBar, { TabItems } from './components/tab-bar';
const Discover: React.FC = () => {
return (
<Stack space={4}>
<TabBar activeTab={TabItems.FIND_GROUPS} />
<Stack space={6}>
<PopularGroups />
<SuggestedGroups />
</Stack>
</Stack>
);
};
export default Discover;

Wyświetl plik

@ -1,78 +1,65 @@
import React, { useEffect } from 'react';
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { createSelector } from 'reselect';
import { fetchGroups } from 'soapbox/actions/groups';
import { openModal } from 'soapbox/actions/modals';
import GroupCard from 'soapbox/components/group-card';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Button, Column, Spinner, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { Button, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { useGroups } from 'soapbox/queries/groups';
import { PERMISSION_CREATE_GROUPS, hasPermission } from 'soapbox/utils/permissions';
import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card';
import type { List as ImmutableList } from 'immutable';
import type { RootState } from 'soapbox/store';
import TabBar, { TabItems } from './components/tab-bar';
import type { Group as GroupEntity } from 'soapbox/types/entities';
const getOrderedGroups = createSelector([
(state: RootState) => state.groups.items,
(state: RootState) => state.groups.isLoading,
(state: RootState) => state.group_relationships,
], (groups, isLoading, group_relationships) => ({
groups: (groups.toList().filter((item: GroupEntity | false) => !!item) as ImmutableList<GroupEntity>)
.map((item) => item.set('relationship', group_relationships.get(item.id) || null))
.filter((item) => item.relationship?.member)
.sort((a, b) => a.display_name.localeCompare(b.display_name)),
isLoading,
}));
// const getOrderedGroups = createSelector([
// (state: RootState) => state.groups.items,
// (state: RootState) => state.group_relationships,
// ], (groups, group_relationships) => ({
// groups: (groups.toList().filter((item: GroupEntity | false) => !!item) as ImmutableList<GroupEntity>)
// .map((item) => item.set('relationship', group_relationships.get(item.id) || null))
// .filter((item) => item.relationship?.member)
// .sort((a, b) => a.display_name.localeCompare(b.display_name)),
// }));
const EmptyMessage = () => (
<Stack space={6} alignItems='center' justifyContent='center' className='h-full p-6'>
<Stack space={2} className='max-w-sm'>
<Text size='2xl' weight='bold' tag='h2' align='center'>
<FormattedMessage
id='groups.empty.title'
defaultMessage='No Groups yet'
/>
</Text>
<Text size='sm' theme='muted' align='center'>
<FormattedMessage
id='groups.empty.subtitle'
defaultMessage='Start discovering groups to join or create your own.'
/>
</Text>
</Stack>
</Stack>
);
const Groups: React.FC = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const { groups, isLoading } = useAppSelector((state) => getOrderedGroups(state));
const canCreateGroup = useAppSelector((state) => hasPermission(state, PERMISSION_CREATE_GROUPS));
useEffect(() => {
dispatch(fetchGroups());
}, []);
const { groups, isLoading } = useGroups();
const createGroup = () => {
dispatch(openModal('MANAGE_GROUP'));
};
if (!groups) {
return (
<Column>
<Spinner />
</Column>
);
}
const emptyMessage = (
<Stack space={6} alignItems='center' justifyContent='center' className='h-full p-6'>
<Stack space={2} className='max-w-sm'>
<Text size='2xl' weight='bold' tag='h2' align='center'>
<FormattedMessage
id='groups.empty.title'
defaultMessage='No Groups yet'
/>
</Text>
<Text size='sm' theme='muted' align='center'>
<FormattedMessage
id='groups.empty.subtitle'
defaultMessage='Start discovering groups to join or create your own.'
/>
</Text>
</Stack>
</Stack>
);
return (
<Stack className='gap-4'>
<Stack space={4}>
{canCreateGroup && (
<Button
className='sm:w-fit sm:self-end xl:hidden'
@ -81,15 +68,20 @@ const Groups: React.FC = () => {
theme='secondary'
block
>
<FormattedMessage id='new_group_panel.action' defaultMessage='Create group' />
<FormattedMessage id='new_group_panel.action' defaultMessage='Create Group' />
</Button>
)}
{features.groupsDiscovery && (
<TabBar activeTab={TabItems.MY_GROUPS} />
)}
<ScrollableList
scrollKey='groups'
emptyMessage={emptyMessage}
emptyMessage={<EmptyMessage />}
itemClassName='py-3 first:pt-0 last:pb-0'
isLoading={isLoading}
showLoading={isLoading && !groups.count()}
showLoading={isLoading && groups.length === 0}
placeholderComponent={PlaceholderGroupCard}
placeholderCount={3}
>

Wyświetl plik

@ -5,23 +5,26 @@ import { HStack, Stack, Text } from 'soapbox/components/ui';
import { generateText, randomIntFromInterval } from '../utils';
const PlaceholderGroupCard = () => {
const groupNameLength = randomIntFromInterval(5, 25);
const roleLength = randomIntFromInterval(5, 15);
const privacyLength = randomIntFromInterval(5, 15);
const groupNameLength = randomIntFromInterval(12, 20);
return (
<div className='animate-pulse overflow-hidden'>
<Stack className='rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900 sm:rounded-xl'>
<div className='relative m-[-1px] mb-0 h-[120px] rounded-t-lg bg-primary-100 dark:bg-gray-800 sm:rounded-t-xl'>
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
<div className='h-16 w-16 rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900' />
</div>
<div className='animate-pulse'>
<Stack className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'>
{/* Group Cover Image */}
<div className='relative grow basis-1/2 rounded-t-lg bg-gray-300 dark:bg-gray-800' />
{/* Group Avatar */}
<div className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2'>
<div className='h-16 w-16 rounded-full bg-gray-500 ring-2 ring-white dark:bg-primary-800 dark:ring-primary-900' />
</div>
<Stack className='p-3 pt-9' alignItems='center' space={3}>
<Text size='lg' weight='bold'>{generateText(groupNameLength)}</Text>
<HStack className='text-gray-700 dark:text-gray-600' space={3} wrap>
<span>{generateText(roleLength)}</span>
<span>{generateText(privacyLength)}</span>
{/* Group Info */}
<Stack alignItems='center' justifyContent='end' grow className='basis-1/2 py-4' space={0.5}>
<Text size='lg' theme='subtle' weight='bold'>{generateText(groupNameLength)}</Text>
<HStack className='text-gray-400 dark:text-gray-600' space={3} wrap>
<span>{generateText(6)}</span>
<span>{generateText(6)}</span>
</HStack>
</Stack>
</Stack>

Wyświetl plik

@ -0,0 +1,38 @@
import React from 'react';
import { HStack, Stack, Text } from 'soapbox/components/ui';
import { generateText, randomIntFromInterval } from '../utils';
const PlaceholderGroupDiscover = () => {
const groupNameLength = randomIntFromInterval(12, 20);
return (
<Stack space={2} className='animate-pulse'>
<Stack className='aspect-w-10 aspect-h-7 h-full w-full overflow-hidden rounded-lg'>
{/* Group Cover Image */}
<div className='absolute inset-0 rounded-t-lg bg-gray-300 object-cover dark:bg-gray-800' />
<Stack justifyContent='end' className='z-10 p-4 text-white' space={3}>
{/* Group Avatar */}
<div className='h-11 w-11 rounded-full bg-gray-500 dark:bg-gray-700 dark:ring-primary-900' />
{/* Group Info */}
<Stack space={1} className='text-gray-500 dark:text-gray-700'>
<Text theme='inherit' weight='bold' truncate>{generateText(groupNameLength)}</Text>
<HStack space={3} wrap>
<Text tag='span' theme='inherit'>{generateText(6)}</Text>
<Text tag='span' theme='inherit'>{generateText(6)}</Text>
</HStack>
</Stack>
</Stack>
</Stack>
{/* Join Group Button */}
<div className='h-10 w-full rounded-full bg-gray-300 dark:bg-gray-800' />
</Stack>
);
};
export default PlaceholderGroupDiscover;

Wyświetl plik

@ -21,7 +21,7 @@ const NewGroupPanel = () => {
<Stack space={2}>
<Stack>
<Text size='lg' weight='bold'>
<FormattedMessage id='new_group_panel.title' defaultMessage='Create New Group' />
<FormattedMessage id='new_group_panel.title' defaultMessage='Create Group' />
</Text>
<Text theme='muted' size='sm'>
@ -30,12 +30,11 @@ const NewGroupPanel = () => {
</Stack>
<Button
icon={require('@tabler/icons/circles.svg')}
onClick={createGroup}
theme='secondary'
block
>
<FormattedMessage id='new_group_panel.action' defaultMessage='Create group' />
<FormattedMessage id='new_group_panel.action' defaultMessage='Create Group' />
</Button>
</Stack>
);

Wyświetl plik

@ -115,6 +115,7 @@ import {
EventDiscussion,
Events,
Groups,
GroupsDiscover,
GroupMembers,
GroupTimeline,
ManageGroup,
@ -282,6 +283,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
{features.groups && <WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} />}
{features.groupsDiscovery && <WrappedRoute path='/groups/discover' exact page={GroupsPage} component={GroupsDiscover} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage' exact page={DefaultPage} component={ManageGroup} content={children} />}

Wyświetl plik

@ -542,6 +542,10 @@ export function Groups() {
return import(/* webpackChunkName: "features/groups" */'../../groups');
}
export function GroupsDiscover() {
return import(/* webpackChunkName: "features/groups/discover" */'../../groups/discover');
}
export function GroupMembers() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-members');
}

Wyświetl plik

@ -987,9 +987,9 @@
"new_event_panel.action": "Create event",
"new_event_panel.subtitle": "Can't find what you're looking for? Schedule your own event.",
"new_event_panel.title": "Create New Event",
"new_group_panel.action": "Create group",
"new_group_panel.action": "Create Group",
"new_group_panel.subtitle": "Can't find what you're looking for? Start your own private or public group.",
"new_group_panel.title": "Create New Group",
"new_group_panel.title": "Create Group",
"notification.favourite": "{name} liked your post",
"notification.follow": "{name} followed you",
"notification.follow_request": "{name} has requested to follow you",

Wyświetl plik

@ -29,6 +29,7 @@ export const GroupRecord = ImmutableRecord({
id: '',
locked: false,
membership_required: false,
members_count: undefined as number | undefined,
note: '',
statuses_visibility: 'public',
uri: '',

Wyświetl plik

@ -0,0 +1,111 @@
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { fetchGroupRelationships } from 'soapbox/actions/groups';
import { importFetchedGroups } from 'soapbox/actions/importer';
import { getNextLink } from 'soapbox/api';
import { useApi, useAppDispatch, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { normalizeGroup } from 'soapbox/normalizers';
import { Group } from 'soapbox/types/entities';
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries';
const GroupKeys = {
myGroups: (userId: string) => ['groups', userId] as const,
popularGroups: ['groups', 'popular'] as const,
suggestedGroups: ['groups', 'suggested'] as const,
};
const useGroups = () => {
const api = useApi();
const account = useOwnAccount();
const dispatch = useAppDispatch();
const getGroups = async (pageParam?: any): Promise<PaginatedResult<Group>> => {
const endpoint = '/api/mock/groups'; // '/api/v1/groups';
const nextPageLink = pageParam?.link;
const uri = nextPageLink || endpoint;
const response = await api.get<Group[]>(uri);
const { data } = response;
const link = getNextLink(response);
const hasMore = !!link;
const result = data.map(normalizeGroup);
// Note: Temporary while part of Groups is using Redux
dispatch(importFetchedGroups(result));
dispatch(fetchGroupRelationships(result.map((item) => item.id)));
return {
result,
hasMore,
link,
};
};
const queryInfo = useInfiniteQuery(
GroupKeys.myGroups(account?.id as string),
({ pageParam }: any) => getGroups(pageParam),
{
enabled: !!account,
keepPreviousData: true,
getNextPageParam: (config) => {
if (config?.hasMore) {
return { nextLink: config?.link };
}
return undefined;
},
});
const data = flattenPages(queryInfo.data);
return {
...queryInfo,
groups: data || [],
};
};
const usePopularGroups = () => {
const api = useApi();
const features = useFeatures();
const getQuery = async () => {
const { data } = await api.get<Group[]>('/api/mock/groups'); // '/api/v1/truth/trends/groups'
const result = data.map(normalizeGroup);
return result;
};
const queryInfo = useQuery<Group[]>(GroupKeys.popularGroups, getQuery, {
enabled: features.groupsDiscovery,
placeholderData: [],
});
return {
groups: queryInfo.data || [],
...queryInfo,
};
};
const useSuggestedGroups = () => {
const api = useApi();
const features = useFeatures();
const getQuery = async () => {
const { data } = await api.get<Group[]>('/api/mock/groups'); // /api/v1/truth/suggestions/groups
const result = data.map(normalizeGroup);
return result;
};
const queryInfo = useQuery<Group[]>(GroupKeys.suggestedGroups, getQuery, {
enabled: features.groupsDiscovery,
placeholderData: [],
});
return {
groups: queryInfo.data || [],
...queryInfo,
};
};
export { useGroups, usePopularGroups, useSuggestedGroups };

Wyświetl plik

@ -499,6 +499,11 @@ const getInstanceFeatures = (instance: Instance) => {
*/
groups: false,
/**
* Can see trending/suggested Groups.
*/
groupsDiscovery: v.software === TRUTHSOCIAL,
/**
* Can hide follows/followers lists and counts.
* @see PATCH /api/v1/accounts/update_credentials

Wyświetl plik

@ -205,6 +205,7 @@
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@tailwindcss/aspect-ratio": "^0.4.2",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.0.3",

Wyświetl plik

@ -100,5 +100,6 @@ module.exports = {
require('@tailwindcss/forms'),
require('@tailwindcss/line-clamp'),
require('@tailwindcss/typography'),
require('@tailwindcss/aspect-ratio'),
],
};

Wyświetl plik

@ -3831,6 +3831,11 @@
resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-2.4.0.tgz#34b1b0d818dc00926b956c3424bff48b89a5b439"
integrity sha512-JZY9Kk3UsQoqp7Rw/BuWw1PrkRwv5h0psjJBbj+Cn9UVyhdzr5vztg2mywXBAJ+jFBUL/pjnVcIvOzKFw4CXng==
"@tailwindcss/aspect-ratio@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@tailwindcss/aspect-ratio/-/aspect-ratio-0.4.2.tgz#9ffd52fee8e3c8b20623ff0dcb29e5c21fb0a9ba"
integrity sha512-8QPrypskfBa7QIMuKHg2TA7BqES6vhBrDLOv8Unb6FcFyd3TjKbc6lcmb9UPQHxfl24sXoJ41ux/H7qQQvfaSQ==
"@tailwindcss/forms@^0.5.3":
version "0.5.3"
resolved "https://registry.yarnpkg.com/@tailwindcss/forms/-/forms-0.5.3.tgz#e4d7989686cbcaf416c53f1523df5225332a86e7"