Merge branch 'manage-group-topics' into 'develop'

Manage group topics

See merge request soapbox-pub/soapbox!2378
This commit is contained in:
Chewbacca 2023-04-17 15:32:29 +00:00
commit bf2f767c8f
18 changed files with 455 additions and 96 deletions

View File

@ -1,12 +0,0 @@
:root {
--reach-tooltip: 1;
}
[data-reach-tooltip] {
@apply pointer-events-none absolute px-2.5 py-1.5 rounded shadow whitespace-nowrap text-xs font-medium bg-gray-800 text-gray-100 dark:bg-gray-100 dark:text-gray-900;
z-index: 100;
}
[data-reach-tooltip-arrow] {
@apply absolute z-50 w-0 h-0 border-l-8 border-solid border-l-transparent border-r-8 border-r-transparent border-b-8 border-b-gray-800 dark:border-b-gray-100;
}

View File

@ -1,67 +1,88 @@
import { TooltipPopup, useTooltip } from '@reach/tooltip'; import {
import React from 'react'; arrow,
FloatingArrow,
import Portal from '../portal/portal'; FloatingPortal,
offset,
import './tooltip.css'; useFloating,
useHover,
useInteractions,
useTransitionStyles,
} from '@floating-ui/react';
import React, { useRef, useState } from 'react';
interface ITooltip { interface ITooltip {
/** Element to display the tooltip around. */
children: React.ReactElement<any, string | React.JSXElementConstructor<any>>
/** Text to display in the tooltip. */ /** Text to display in the tooltip. */
text: string text: string
/** Element to display the tooltip around. */
children: React.ReactNode
} }
const centered = (triggerRect: any, tooltipRect: any) => { /**
const triggerCenter = triggerRect.left + triggerRect.width / 2; * Tooltip
const left = triggerCenter - tooltipRect.width / 2; */
const maxLeft = window.innerWidth - tooltipRect.width - 2; const Tooltip: React.FC<ITooltip> = (props) => {
return { const { children, text } = props;
left: Math.min(Math.max(2, left), maxLeft) + window.scrollX,
top: triggerRect.bottom + 8 + window.scrollY,
};
};
/** Hoverable tooltip element. */ const [isOpen, setIsOpen] = useState<boolean>(false);
const Tooltip: React.FC<ITooltip> = ({
children,
text,
}) => {
// get the props from useTooltip
const [trigger, tooltip] = useTooltip();
// destructure off what we need to position the triangle const arrowRef = useRef<SVGSVGElement>(null);
const { isVisible, triggerRect } = tooltip;
const { x, y, strategy, refs, context } = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement: 'top',
middleware: [
offset(6),
arrow({
element: arrowRef,
}),
],
});
const hover = useHover(context);
const { isMounted, styles } = useTransitionStyles(context, {
initial: {
opacity: 0,
transform: 'scale(0.8)',
},
duration: {
open: 200,
close: 200,
},
});
const { getReferenceProps, getFloatingProps } = useInteractions([
hover,
]);
return ( return (
<React.Fragment> <>
{React.cloneElement(children as any, trigger)} {React.cloneElement(children, {
ref: refs.setReference,
...getReferenceProps(),
})}
{isVisible && ( {(isMounted) && (
// The Triangle. We position it relative to the trigger, not the popup <FloatingPortal>
// so that collisions don't have a triangle pointing off to nowhere.
// Using a Portal may seem a little extreme, but we can keep the
// positioning logic simpler here instead of needing to consider
// the popup's position relative to the trigger and collisions
<Portal>
<div <div
data-reach-tooltip-arrow='true' ref={refs.setFloating}
style={{ style={{
left: position: strategy,
triggerRect && triggerRect.left - 10 + triggerRect.width / 2 as any, top: y ?? 0,
top: triggerRect && triggerRect.bottom + window.scrollY as any, left: x ?? 0,
...styles,
}} }}
/> className='pointer-events-none z-[100] whitespace-nowrap rounded bg-gray-800 px-2.5 py-1.5 text-xs font-medium text-gray-100 shadow dark:bg-gray-100 dark:text-gray-900'
</Portal> {...getFloatingProps()}
>
{text}
<FloatingArrow ref={arrowRef} context={context} className='fill-gray-800 dark:fill-gray-100' />
</div>
</FloatingPortal>
)} )}
<TooltipPopup </>
{...tooltip}
label={text}
aria-label={text}
position={centered}
/>
</React.Fragment>
); );
}; };
export default Tooltip; export default Tooltip;

View File

@ -12,8 +12,9 @@ interface UseEntityActionsOpts<TEntity extends Entity = Entity> {
} }
interface EntityActionEndpoints { interface EntityActionEndpoints {
post?: string
delete?: string delete?: string
patch?: string
post?: string
} }
function useEntityActions<TEntity extends Entity = Entity, Data = any>( function useEntityActions<TEntity extends Entity = Entity, Data = any>(
@ -30,10 +31,14 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
const { createEntity, isSubmitting: createSubmitting } = const { createEntity, isSubmitting: createSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts); useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
const { createEntity: updateEntity, isSubmitting: updateSubmitting } =
useCreateEntity<TEntity, Data>(path, (data) => api.patch(endpoints.patch!, data), opts);
return { return {
createEntity, createEntity,
deleteEntity, deleteEntity,
isSubmitting: createSubmitting || deleteSubmitting, updateEntity,
isSubmitting: createSubmitting || deleteSubmitting || updateSubmitting,
}; };
} }

View File

@ -0,0 +1,175 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { HStack, IconButton, Stack, Text, Tooltip } from 'soapbox/components/ui';
import { importEntities } from 'soapbox/entity-store/actions';
import { Entities } from 'soapbox/entity-store/entities';
import { useAppDispatch } from 'soapbox/hooks';
import { useUpdateGroupTag } from 'soapbox/hooks/api';
import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast';
import { shortNumberFormat } from 'soapbox/utils/numbers';
import type { Group, GroupTag } from 'soapbox/schemas';
const messages = defineMessages({
hideTag: { id: 'group.tags.hide', defaultMessage: 'Hide topic' },
showTag: { id: 'group.tags.show', defaultMessage: 'Show topic' },
total: { id: 'group.tags.total', defaultMessage: 'Total Posts' },
pinTag: { id: 'group.tags.pin', defaultMessage: 'Pin topic' },
unpinTag: { id: 'group.tags.unpin', defaultMessage: 'Unpin topic' },
pinSuccess: { id: 'group.tags.pin.success', defaultMessage: 'Pinned!' },
unpinSuccess: { id: 'group.tags.unpin.success', defaultMessage: 'Unpinned!' },
visibleSuccess: { id: 'group.tags.visible.success', defaultMessage: 'Topic marked as visible' },
hiddenSuccess: { id: 'group.tags.hidden.success', defaultMessage: 'Topic marked as hidden' },
});
interface IGroupMemberListItem {
tag: GroupTag
group: Group
isPinnable: boolean
}
const GroupTagListItem = (props: IGroupMemberListItem) => {
const { group, tag, isPinnable } = props;
const dispatch = useAppDispatch();
const intl = useIntl();
const { updateGroupTag } = useUpdateGroupTag(group.id, tag.id);
const isOwner = group.relationship?.role === GroupRoles.OWNER;
const isAdmin = group.relationship?.role === GroupRoles.ADMIN;
const canEdit = isOwner || isAdmin;
const toggleVisibility = () => {
updateGroupTag({
group_tag_type: tag.visible ? 'hidden' : 'normal',
}, {
onSuccess() {
const entity = {
...tag,
visible: !tag.visible,
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success(
entity.visible ?
intl.formatMessage(messages.visibleSuccess) :
intl.formatMessage(messages.hiddenSuccess),
);
},
});
};
const togglePin = () => {
updateGroupTag({
group_tag_type: tag.pinned ? 'normal' : 'pinned',
}, {
onSuccess() {
const entity = {
...tag,
pinned: !tag.pinned,
};
dispatch(importEntities([entity], Entities.GROUP_TAGS));
toast.success(
entity.pinned ?
intl.formatMessage(messages.pinSuccess) :
intl.formatMessage(messages.unpinSuccess),
);
},
});
};
const renderPinIcon = () => {
if (isPinnable) {
return (
<Tooltip
text={
tag.pinned ?
intl.formatMessage(messages.unpinTag) :
intl.formatMessage(messages.pinTag)
}
>
<IconButton
onClick={togglePin}
theme='transparent'
src={
tag.pinned ?
require('@tabler/icons/pin-filled.svg') :
require('@tabler/icons/pin.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
);
}
if (!isPinnable && tag.pinned) {
return (
<Tooltip text={intl.formatMessage(messages.unpinTag)}>
<IconButton
onClick={togglePin}
theme='transparent'
src={require('@tabler/icons/pin-filled.svg')}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
);
}
};
return (
<HStack alignItems='center' justifyContent='between'>
<Link to={`/groups/${group.id}/tag/${tag.id}`} className='group grow'>
<Stack>
<Text
weight='bold'
theme={(tag.visible || !canEdit) ? 'default' : 'subtle'}
className='group-hover:underline'
>
#{tag.name}
</Text>
<Text size='sm' theme={(tag.visible || !canEdit) ? 'muted' : 'subtle'}>
{intl.formatMessage(messages.total)}:
{' '}
<Text size='sm' theme='inherit' weight='semibold' tag='span'>
{shortNumberFormat(tag.uses)}
</Text>
</Text>
</Stack>
</Link>
{canEdit ? (
<HStack alignItems='center' space={2}>
{tag.visible ? (
renderPinIcon()
) : null}
<Tooltip
text={
tag.visible ?
intl.formatMessage(messages.hideTag) :
intl.formatMessage(messages.showTag)
}
>
<IconButton
onClick={toggleVisibility}
theme='transparent'
src={
tag.visible ?
require('@tabler/icons/eye.svg') :
require('@tabler/icons/eye-off.svg')
}
iconClassName='h-5 w-5 text-primary-500 dark:text-accent-blue'
/>
</Tooltip>
</HStack>
) : null}
</HStack>
);
};
export default GroupTagListItem;

View File

@ -0,0 +1,30 @@
import React from 'react';
import { Column } from 'soapbox/components/ui';
import { useGroup, useGroupTag } from 'soapbox/hooks/api';
type RouteParams = { id: string, groupId: string };
interface IGroupTimeline {
params: RouteParams
}
const GroupTagTimeline: React.FC<IGroupTimeline> = (props) => {
const groupId = props.params.groupId;
const tagId = props.params.id;
const { group } = useGroup(groupId);
const { tag } = useGroupTag(tagId);
if (!group) {
return null;
}
return (
<Column label={`#${tag}`}>
{/* TODO */}
</Column>
);
};
export default GroupTagTimeline;

View File

@ -0,0 +1,69 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import ScrollableList from 'soapbox/components/scrollable-list';
import { Icon, Stack, Text } from 'soapbox/components/ui';
import { useGroupTags } from 'soapbox/hooks/api';
import { useGroup } from 'soapbox/queries/groups';
import PlaceholderAccount from '../placeholder/components/placeholder-account';
import GroupTagListItem from './components/group-tag-list-item';
import type { Group } from 'soapbox/types/entities';
interface IGroupTopics {
params: { id: string }
}
const GroupTopics: React.FC<IGroupTopics> = (props) => {
const groupId = props.params.id;
const { group, isFetching: isFetchingGroup } = useGroup(groupId);
const { tags, isFetching: isFetchingTags, hasNextPage, fetchNextPage } = useGroupTags(groupId);
const isLoading = isFetchingGroup || isFetchingTags;
const pinnedTags = tags.filter((tag) => tag.pinned);
const isPinnable = pinnedTags.length < 3;
return (
<ScrollableList
scrollKey='group-tags'
hasMore={hasNextPage}
onLoadMore={fetchNextPage}
isLoading={isLoading || !group}
showLoading={!group || isLoading && tags.length === 0}
placeholderComponent={PlaceholderAccount}
placeholderCount={3}
className='divide-y divide-solid divide-gray-300'
itemClassName='py-3 last:pb-0'
emptyMessage={
<Stack space={4} className='pt-6' justifyContent='center' alignItems='center'>
<div className='rounded-full bg-gray-200 p-4 dark:bg-gray-800'>
<Icon
src={require('@tabler/icons/hash.svg')}
className='h-6 w-6 text-gray-600'
/>
</div>
<Text theme='muted'>
<FormattedMessage id='group.tags.empty' defaultMessage='There are no topics in this group yet.' />
</Text>
</Stack>
}
emptyMessageCard={false}
>
{tags.map((tag) => (
<GroupTagListItem
key={tag.id}
group={group as Group}
isPinnable={isPinnable}
tag={tag}
/>
))}
</ScrollableList>
);
};
export default GroupTopics;

View File

@ -5,9 +5,8 @@ import { useHistory } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import List, { ListItem } from 'soapbox/components/list'; import List, { ListItem } from 'soapbox/components/list';
import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui'; import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui';
import { useAppDispatch, useGroupsPath } from 'soapbox/hooks'; import { useAppDispatch, useBackend, useGroupsPath } from 'soapbox/hooks';
import { useDeleteGroup, useGroup } from 'soapbox/hooks/api'; import { useDeleteGroup, useGroup } from 'soapbox/hooks/api';
import { useBackend } from 'soapbox/hooks/useBackend';
import { GroupRoles } from 'soapbox/schemas/group-member'; import { GroupRoles } from 'soapbox/schemas/group-member';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
import { TRUTHSOCIAL } from 'soapbox/utils/features'; import { TRUTHSOCIAL } from 'soapbox/utils/features';

View File

@ -126,6 +126,8 @@ import {
GroupsTags, GroupsTags,
PendingGroupRequests, PendingGroupRequests,
GroupMembers, GroupMembers,
GroupTags,
GroupTagTimeline,
GroupTimeline, GroupTimeline,
ManageGroup, ManageGroup,
GroupBlockedMembers, GroupBlockedMembers,
@ -301,6 +303,8 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
{features.groupsDiscovery && <WrappedRoute path='/groups/tags' exact page={GroupsPendingPage} component={GroupsTags} content={children} />} {features.groupsDiscovery && <WrappedRoute path='/groups/tags' exact page={GroupsPendingPage} component={GroupsTags} content={children} />}
{features.groupsDiscovery && <WrappedRoute path='/groups/discover/tags/:id' exact page={GroupsPendingPage} component={GroupsTag} content={children} />} {features.groupsDiscovery && <WrappedRoute path='/groups/discover/tags/:id' exact page={GroupsPendingPage} component={GroupsTag} content={children} />}
{features.groupsPending && <WrappedRoute path='/groups/pending-requests' exact page={GroupsPendingPage} component={PendingGroupRequests} content={children} />} {features.groupsPending && <WrappedRoute path='/groups/pending-requests' exact page={GroupsPendingPage} component={PendingGroupRequests} content={children} />}
{features.groupsTags && <WrappedRoute path='/groups/:id/tags' exact page={GroupPage} component={GroupTags} content={children} />}
{features.groupsTags && <WrappedRoute path='/groups/:groupId/tag/:id' exact page={GroupsPendingPage} component={GroupTagTimeline} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} 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/members' exact page={GroupPage} component={GroupMembers} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/media' publicRoute={!authenticatedProfile} component={GroupGallery} page={GroupPage} content={children} />} {features.groups && <WrappedRoute path='/groups/:id/media' publicRoute={!authenticatedProfile} component={GroupGallery} page={GroupPage} content={children} />}

View File

@ -578,6 +578,14 @@ export function GroupMembers() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-members'); return import(/* webpackChunkName: "features/groups" */'../../group/group-members');
} }
export function GroupTags() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-tags');
}
export function GroupTagTimeline() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-tag-timeline');
}
export function GroupTimeline() { export function GroupTimeline() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline'); return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
} }

View File

@ -0,0 +1,23 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntities } from 'soapbox/entity-store/hooks';
import { useApi } from 'soapbox/hooks/useApi';
import { groupTagSchema } from 'soapbox/schemas';
import type { GroupTag } from 'soapbox/schemas';
function useGroupTags(groupId: string) {
const api = useApi();
const { entities, ...result } = useEntities<GroupTag>(
[Entities.GROUP_TAGS, groupId],
() => api.get(`api/v1/truth/trends/groups/${groupId}/tags`),
{ schema: groupTagSchema },
);
return {
...result,
tags: entities,
};
}
export { useGroupTags };

View File

@ -0,0 +1,18 @@
import { Entities } from 'soapbox/entity-store/entities';
import { useEntityActions } from 'soapbox/entity-store/hooks';
import type { GroupTag } from 'soapbox/schemas';
function useUpdateGroupTag(groupId: string, tagId: string) {
const { updateEntity, ...rest } = useEntityActions<GroupTag>(
[Entities.GROUP_TAGS, groupId, tagId],
{ patch: `/api/v1/groups/${groupId}/tags/${tagId}` },
);
return {
updateGroupTag: updateEntity,
...rest,
};
}
export { useUpdateGroupTag };

View File

@ -11,11 +11,12 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest'
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup'; export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDeleteGroup } from './groups/useDeleteGroup';
export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
export { useGroupMedia } from './groups/useGroupMedia';
export { useGroup, useGroups } from './groups/useGroups'; export { useGroup, useGroups } from './groups/useGroups';
export { useGroupMedia } from './groups/useGroupMedia';
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
export { useGroupSearch } from './groups/useGroupSearch'; export { useGroupSearch } from './groups/useGroupSearch';
export { useGroupTag } from './groups/useGroupTag'; export { useGroupTag } from './groups/useGroupTag';
export { useGroupTags } from './groups/useGroupTags';
export { useGroupValidation } from './groups/useGroupValidation'; export { useGroupValidation } from './groups/useGroupValidation';
export { useGroupsFromTag } from './groups/useGroupsFromTag'; export { useGroupsFromTag } from './groups/useGroupsFromTag';
export { useJoinGroup } from './groups/useJoinGroup'; export { useJoinGroup } from './groups/useJoinGroup';
@ -23,8 +24,9 @@ export { useLeaveGroup } from './groups/useLeaveGroup';
export { usePopularTags } from './groups/usePopularTags'; export { usePopularTags } from './groups/usePopularTags';
export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember';
export { useUpdateGroup } from './groups/useUpdateGroup'; export { useUpdateGroup } from './groups/useUpdateGroup';
export { useUpdateGroupTag } from './groups/useUpdateGroupTag';
/** /**
* Relationships * Relationships
*/ */
export { useRelationships } from './useRelationships'; export { useRelationships } from './useRelationships';

View File

@ -2,6 +2,7 @@ export { useAccount } from './useAccount';
export { useApi } from './useApi'; export { useApi } from './useApi';
export { useAppDispatch } from './useAppDispatch'; export { useAppDispatch } from './useAppDispatch';
export { useAppSelector } from './useAppSelector'; export { useAppSelector } from './useAppSelector';
export { useBackend } from './useBackend';
export { useClickOutside } from './useClickOutside'; export { useClickOutside } from './useClickOutside';
export { useCompose } from './useCompose'; export { useCompose } from './useCompose';
export { useDebounce } from './useDebounce'; export { useDebounce } from './useDebounce';

View File

@ -809,8 +809,19 @@
"group.tabs.all": "All", "group.tabs.all": "All",
"group.tabs.media": "Media", "group.tabs.media": "Media",
"group.tabs.members": "Members", "group.tabs.members": "Members",
"group.tabs.tags": "Topics",
"group.tags.empty": "There are no topics in this group yet.",
"group.tags.hidden.success": "Topic marked as hidden",
"group.tags.hide": "Hide topic",
"group.tags.hint": "Add up to 3 keywords that will serve as core topics of discussion in the group.", "group.tags.hint": "Add up to 3 keywords that will serve as core topics of discussion in the group.",
"group.tags.label": "Tags", "group.tags.label": "Tags",
"group.tags.pin": "Pin topic",
"group.tags.pin.success": "Pinned!",
"group.tags.show": "Show topic",
"group.tags.total": "Total Posts",
"group.tags.unpin": "Unpin topic",
"group.tags.unpin.success": "Unpinned!",
"group.tags.visible.success": "Topic marked as visible",
"group.update.success": "Group successfully saved", "group.update.success": "Group successfully saved",
"group.upload_banner": "Upload photo", "group.upload_banner": "Upload photo",
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.", "groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { useRouteMatch } from 'react-router-dom'; import { useRouteMatch } from 'react-router-dom';
@ -12,7 +12,7 @@ import {
SignUpPanel, SignUpPanel,
SuggestedGroupsPanel, SuggestedGroupsPanel,
} from 'soapbox/features/ui/util/async-components'; } from 'soapbox/features/ui/util/async-components';
import { useOwnAccount } from 'soapbox/hooks'; import { useFeatures, useOwnAccount } from 'soapbox/hooks';
import { useGroup } from 'soapbox/hooks/api'; import { useGroup } from 'soapbox/hooks/api';
import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests'; import { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests';
import { Group } from 'soapbox/schemas'; import { Group } from 'soapbox/schemas';
@ -23,6 +23,7 @@ const messages = defineMessages({
all: { id: 'group.tabs.all', defaultMessage: 'All' }, all: { id: 'group.tabs.all', defaultMessage: 'All' },
members: { id: 'group.tabs.members', defaultMessage: 'Members' }, members: { id: 'group.tabs.members', defaultMessage: 'Members' },
media: { id: 'group.tabs.media', defaultMessage: 'Media' }, media: { id: 'group.tabs.media', defaultMessage: 'Media' },
tags: { id: 'group.tabs.tags', defaultMessage: 'Topics' },
}); });
interface IGroupPage { interface IGroupPage {
@ -61,6 +62,7 @@ const BlockedBlankslate = ({ group }: { group: Group }) => (
/** Page to display a group. */ /** Page to display a group. */
const GroupPage: React.FC<IGroupPage> = ({ params, children }) => { const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
const intl = useIntl(); const intl = useIntl();
const features = useFeatures();
const match = useRouteMatch(); const match = useRouteMatch();
const me = useOwnAccount(); const me = useOwnAccount();
@ -73,13 +75,29 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
const isBlocked = group?.relationship?.blocked_by; const isBlocked = group?.relationship?.blocked_by;
const isPrivate = group?.locked; const isPrivate = group?.locked;
const items = [ // if ((group as any) === false) {
{ // return (
// <MissingIndicator />
// );
// }
const tabItems = useMemo(() => {
const items = [];
items.push({
text: intl.formatMessage(messages.all), text: intl.formatMessage(messages.all),
to: `/groups/${group?.id}`, to: `/groups/${group?.id}`,
name: '/groups/:id', name: '/groups/:id',
}, });
{
if (features.groupsTags) {
items.push({
text: intl.formatMessage(messages.tags),
to: `/groups/${group?.id}/tags`,
name: '/groups/:id/tags',
});
}
items.push({
text: intl.formatMessage(messages.members), text: intl.formatMessage(messages.members),
to: `/groups/${group?.id}/members`, to: `/groups/${group?.id}/members`,
name: '/groups/:id/members', name: '/groups/:id/members',
@ -89,8 +107,10 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
text: intl.formatMessage(messages.media), text: intl.formatMessage(messages.media),
to: `/groups/${group?.id}/media`, to: `/groups/${group?.id}/media`,
name: '/groups/:id/media', name: '/groups/:id/media',
}, });
];
return items;
}, [features.groupsTags]);
const renderChildren = () => { const renderChildren = () => {
if (!isMember && isPrivate) { if (!isMember && isPrivate) {
@ -109,7 +129,7 @@ const GroupPage: React.FC<IGroupPage> = ({ params, children }) => {
<GroupHeader group={group} /> <GroupHeader group={group} />
<Tabs <Tabs
items={items} items={tabItems}
activeItem={match.path} activeItem={match.path}
/> />

View File

@ -559,6 +559,11 @@ const getInstanceFeatures = (instance: Instance) => {
*/ */
groupsSearch: v.software === TRUTHSOCIAL, groupsSearch: v.software === TRUTHSOCIAL,
/**
* Can see topics for Groups.
*/
groupsTags: v.software === TRUTHSOCIAL,
/** /**
* Can validate group names. * Can validate group names.
*/ */

View File

@ -61,7 +61,6 @@
"@reach/popover": "^0.18.0", "@reach/popover": "^0.18.0",
"@reach/rect": "^0.18.0", "@reach/rect": "^0.18.0",
"@reach/tabs": "^0.18.0", "@reach/tabs": "^0.18.0",
"@reach/tooltip": "^0.18.0",
"@reduxjs/toolkit": "^1.8.1", "@reduxjs/toolkit": "^1.8.1",
"@sentry/browser": "^7.37.2", "@sentry/browser": "^7.37.2",
"@sentry/react": "^7.37.2", "@sentry/react": "^7.37.2",

View File

@ -2749,30 +2749,11 @@
"@reach/polymorphic" "0.18.0" "@reach/polymorphic" "0.18.0"
"@reach/utils" "0.18.0" "@reach/utils" "0.18.0"
"@reach/tooltip@^0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@reach/tooltip/-/tooltip-0.18.0.tgz#6d416e77a82543af9a57d122962f9c0294fc2a5f"
integrity sha512-yugoTmTjB3qoMk/nUvcnw99MqpyE2TQMOXE29qnQhSqHriRwQhfftjXlTAGTSzsUJmbyms3A/1gQW0X61kjFZw==
dependencies:
"@reach/auto-id" "0.18.0"
"@reach/polymorphic" "0.18.0"
"@reach/portal" "0.18.0"
"@reach/rect" "0.18.0"
"@reach/utils" "0.18.0"
"@reach/visually-hidden" "0.18.0"
"@reach/utils@0.18.0": "@reach/utils@0.18.0":
version "0.18.0" version "0.18.0"
resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee"
integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A== integrity sha512-KdVMdpTgDyK8FzdKO9SCpiibuy/kbv3pwgfXshTI6tEcQT1OOwj7BAksnzGC0rPz0UholwC+AgkqEl3EJX3M1A==
"@reach/visually-hidden@0.18.0":
version "0.18.0"
resolved "https://registry.yarnpkg.com/@reach/visually-hidden/-/visually-hidden-0.18.0.tgz#17923c08acc5946624c2836b2b09d359b3aa8c27"
integrity sha512-NsJ3oeHJtPc6UOeV6MHMuzQ5sl1ouKhW85i3C0S7VM+klxVlYScBZ2J4UVnWB50A2c+evdVpCnld2YeuyYYwBw==
dependencies:
"@reach/polymorphic" "0.18.0"
"@reduxjs/toolkit@^1.8.1": "@reduxjs/toolkit@^1.8.1":
version "1.8.1" version "1.8.1"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.1.tgz#94ee1981b8cf9227cda40163a04704a9544c9a9f" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.1.tgz#94ee1981b8cf9227cda40163a04704a9544c9a9f"