diff --git a/app/soapbox/features/groups/tag.tsx b/app/soapbox/features/groups/tag.tsx
new file mode 100644
index 000000000..4774a4700
--- /dev/null
+++ b/app/soapbox/features/groups/tag.tsx
@@ -0,0 +1,117 @@
+import clsx from 'clsx';
+import React, { useCallback, useState } from 'react';
+import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
+
+import { Column, HStack, Icon } from 'soapbox/components/ui';
+import { useGroupTag, useGroupsFromTag } from 'soapbox/hooks/api';
+
+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
;
+});
+
+interface ITag {
+ params: { id: string }
+}
+
+const Tag: React.FC = (props) => {
+ const tagId = props.params.id;
+
+ const [layout, setLayout] = useState(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) => (
+
+
+
+ ), []);
+
+ const renderGroupGrid = useCallback((group: Group, index: number) => (
+
+
+
+ ), []);
+
+ if (isLoading || !tag) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ }
+ >
+ {layout === Layout.LIST ? (
+ renderGroupList(group, index)}
+ endReached={handleLoadMore}
+ />
+ ) : (
+ renderGroupGrid(group, index)}
+ components={{
+ Item: (props) => (
+
+ ),
+ List: GridList,
+ }}
+ endReached={handleLoadMore}
+ />
+ )}
+
+ );
+};
+
+export default Tag;
diff --git a/app/soapbox/features/groups/tags.tsx b/app/soapbox/features/groups/tags.tsx
index 052f5a6e3..1484665e4 100644
--- a/app/soapbox/features/groups/tags.tsx
+++ b/app/soapbox/features/groups/tags.tsx
@@ -1,35 +1,24 @@
import clsx from 'clsx';
-import React, { useCallback, useState } from 'react';
-import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso';
+import React from 'react';
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+import { Virtuoso } from 'react-virtuoso';
-import { Column, HStack, Icon } from 'soapbox/components/ui';
-import { useGroupsFromTag } from 'soapbox/hooks/api';
+import { Column, Text } from 'soapbox/components/ui';
+import { usePopularTags } from 'soapbox/hooks/api';
-import GroupGridItem from './components/discover/group-grid-item';
-import GroupListItem from './components/discover/group-list-item';
+import TagListItem from './components/discover/tag-list-item';
-import type { Group } from 'soapbox/schemas';
+import type { GroupTag } from 'soapbox/schemas';
-enum Layout {
- LIST = 'LIST',
- GRID = 'GRID'
-}
-
-const GridList: Components['List'] = React.forwardRef((props, ref) => {
- const { context, ...rest } = props;
- return ;
+const messages = defineMessages({
+ title: { id: 'groups.tags.title', defaultMessage: 'Browse Topics' },
});
-interface ITags {
- params: { id: string }
-}
+const Tags: React.FC = () => {
+ const intl = useIntl();
-const Tags: React.FC = (props) => {
- const tagId = props.params.id;
-
- const [layout, setLayout] = useState(Layout.LIST);
-
- const { groups, hasNextPage, fetchNextPage } = useGroupsFromTag(tagId);
+ const { tags, isFetched, isError, hasNextPage, fetchNextPage } = usePopularTags();
+ const isEmpty = (isFetched && tags.length === 0) || isError;
const handleLoadMore = () => {
if (hasNextPage) {
@@ -37,7 +26,7 @@ const Tags: React.FC = (props) => {
}
};
- const renderGroupList = useCallback((group: Group, index: number) => (
+ const renderItem = (index: number, tag: GroupTag) => (
= (props) => {
})
}
>
-
+
- ), []);
-
- const renderGroupGrid = useCallback((group: Group, index: number) => (
-
-
-
- ), []);
+ );
return (
-
-
-
-
-
- }
- >
- {layout === Layout.LIST ? (
+
+ {isEmpty ? (
+
+
+
+ ) : (
renderGroupList(group, index)}
- endReached={handleLoadMore}
- />
- ) : (
- renderGroupGrid(group, index)}
- components={{
- Item: (props) => (
-
- ),
- List: GridList,
- }}
+ data={tags}
+ itemContent={renderItem}
endReached={handleLoadMore}
/>
)}
diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx
index 15cdc8033..b967f27de 100644
--- a/app/soapbox/features/ui/index.tsx
+++ b/app/soapbox/features/ui/index.tsx
@@ -122,6 +122,7 @@ import {
GroupsDiscover,
GroupsPopular,
GroupsSuggested,
+ GroupsTag,
GroupsTags,
PendingGroupRequests,
GroupMembers,
@@ -297,7 +298,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) =>
{features.groupsDiscovery && }
{features.groupsDiscovery && }
{features.groupsDiscovery && }
- {features.groupsDiscovery && }
+ {features.groupsDiscovery && }
+ {features.groupsDiscovery && }
{features.groupsPending && }
{features.groups && }
{features.groups && }
diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts
index 01e44ca0a..654cab76d 100644
--- a/app/soapbox/features/ui/util/async-components.ts
+++ b/app/soapbox/features/ui/util/async-components.ts
@@ -562,6 +562,10 @@ export function GroupsSuggested() {
return import(/* webpackChunkName: "features/groups" */'../../groups/suggested');
}
+export function GroupsTag() {
+ return import(/* webpackChunkName: "features/groups" */'../../groups/tag');
+}
+
export function GroupsTags() {
return import(/* webpackChunkName: "features/groups" */'../../groups/tags');
}
diff --git a/app/soapbox/hooks/api/groups/useGroupTag.ts b/app/soapbox/hooks/api/groups/useGroupTag.ts
new file mode 100644
index 000000000..d0e63d74d
--- /dev/null
+++ b/app/soapbox/hooks/api/groups/useGroupTag.ts
@@ -0,0 +1,21 @@
+import { Entities } from 'soapbox/entity-store/entities';
+import { useEntity } from 'soapbox/entity-store/hooks';
+import { useApi } from 'soapbox/hooks';
+import { type GroupTag, groupTagSchema } from 'soapbox/schemas';
+
+function useGroupTag(tagId: string) {
+ const api = useApi();
+
+ const { entity: tag, ...result } = useEntity(
+ [Entities.GROUP_TAGS, tagId],
+ () => api.get(`/api/v1/tags/${tagId }`),
+ { schema: groupTagSchema },
+ );
+
+ return {
+ ...result,
+ tag,
+ };
+}
+
+export { useGroupTag };
\ No newline at end of file
diff --git a/app/soapbox/hooks/api/groups/useGroupsFromTag.ts b/app/soapbox/hooks/api/groups/useGroupsFromTag.ts
index 7ebf871f4..a6b8540dc 100644
--- a/app/soapbox/hooks/api/groups/useGroupsFromTag.ts
+++ b/app/soapbox/hooks/api/groups/useGroupsFromTag.ts
@@ -5,6 +5,8 @@ import { groupSchema } from 'soapbox/schemas';
import { useApi } from '../../useApi';
import { useFeatures } from '../../useFeatures';
+import { useGroupRelationships } from './useGroups';
+
import type { Group } from 'soapbox/schemas';
function useGroupsFromTag(tagId: string) {
@@ -13,16 +15,22 @@ function useGroupsFromTag(tagId: string) {
const { entities, ...result } = useEntities(
[Entities.GROUPS, 'tags', tagId],
- () => api.get(`/api/mock/tags/${tagId}/groups`),
+ () => api.get(`/api/v1/tags/${tagId}/groups`),
{
schema: groupSchema,
enabled: features.groupsDiscovery,
},
);
+ const { relationships } = useGroupRelationships(entities.map(entity => entity.id));
+
+ const groups = entities.map((group) => ({
+ ...group,
+ relationship: relationships[group.id] || null,
+ }));
return {
...result,
- groups: entities,
+ groups,
};
}
diff --git a/app/soapbox/hooks/api/groups/usePopularTags.ts b/app/soapbox/hooks/api/groups/usePopularTags.ts
index d6fe5e0af..0bd272a2d 100644
--- a/app/soapbox/hooks/api/groups/usePopularTags.ts
+++ b/app/soapbox/hooks/api/groups/usePopularTags.ts
@@ -11,7 +11,7 @@ function usePopularTags() {
const { entities, ...result } = useEntities(
[Entities.GROUP_TAGS],
- () => api.get('/api/mock/groups/tags'),
+ () => api.get('/api/v1/groups/tags'),
{
schema: groupTagSchema,
enabled: features.groupsDiscovery,
diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts
index f28c6a5a5..800ea5857 100644
--- a/app/soapbox/hooks/api/index.ts
+++ b/app/soapbox/hooks/api/index.ts
@@ -15,6 +15,7 @@ export { useGroupMedia } from './groups/useGroupMedia';
export { useGroup, useGroups } from './groups/useGroups';
export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests';
export { useGroupSearch } from './groups/useGroupSearch';
+export { useGroupTag } from './groups/useGroupTag';
export { useGroupValidation } from './groups/useGroupValidation';
export { useGroupsFromTag } from './groups/useGroupsFromTag';
export { useJoinGroup } from './groups/useJoinGroup';
diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json
index 89608c4d5..2d0c10823 100644
--- a/app/soapbox/locales/en.json
+++ b/app/soapbox/locales/en.json
@@ -830,6 +830,10 @@
"groups.discover.suggested.empty": "Unable to fetch suggested groups at this time. Please check back later.",
"groups.discover.suggested.show_more": "Show More",
"groups.discover.suggested.title": "Suggested For You",
+ "groups.discover.tags.empty": "Unable to fetch popular topics at this time. Please check back later.",
+ "groups.discover.tags.show_more": "Show More",
+ "groups.discover.tags.title": "Browse Topics",
+ "groups.discovery.tags.no_of_groups": "Number of groups",
"groups.empty.subtitle": "Start discovering groups to join or create your own.",
"groups.empty.title": "No Groups yet",
"groups.pending.count": "{number, plural, one {# pending request} other {# pending requests}}",
@@ -838,6 +842,7 @@
"groups.pending.label": "Pending Requests",
"groups.popular.label": "Suggested Groups",
"groups.search.placeholder": "Search My Groups",
+ "groups.tags.title": "Browse Topics",
"hashtag.column_header.tag_mode.all": "and {additional}",
"hashtag.column_header.tag_mode.any": "or {additional}",
"hashtag.column_header.tag_mode.none": "without {additional}",