+type AsyncComponent = () => Promise<{ default: React.ComponentType
}>
+
+const withHoc =
(asyncComponent: AsyncComponent
, hoc: HOC
) => {
+ return async () => {
+ const { default: component } = await asyncComponent();
+ return { default: hoc(component) };
+ };
+};
+
+export default withHoc;
\ No newline at end of file
diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx
index 752e73636..24131159d 100644
--- a/app/soapbox/components/status.tsx
+++ b/app/soapbox/components/status.tsx
@@ -253,7 +253,7 @@ const Status: React.FC = (props) => {
return (
}
text={
diff --git a/app/soapbox/entity-store/hooks/index.ts b/app/soapbox/entity-store/hooks/index.ts
index d113c505a..b95d2d1af 100644
--- a/app/soapbox/entity-store/hooks/index.ts
+++ b/app/soapbox/entity-store/hooks/index.ts
@@ -1,6 +1,7 @@
export { useEntities } from './useEntities';
export { useEntity } from './useEntity';
export { useEntityActions } from './useEntityActions';
+export { useEntityLookup } from './useEntityLookup';
export { useCreateEntity } from './useCreateEntity';
export { useDeleteEntity } from './useDeleteEntity';
export { useDismissEntity } from './useDismissEntity';
diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts
index 37027f73f..63447ae67 100644
--- a/app/soapbox/entity-store/hooks/useEntity.ts
+++ b/app/soapbox/entity-store/hooks/useEntity.ts
@@ -59,4 +59,5 @@ function useEntity(
export {
useEntity,
+ type UseEntityOpts,
};
\ No newline at end of file
diff --git a/app/soapbox/entity-store/hooks/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts
new file mode 100644
index 000000000..a49a659a4
--- /dev/null
+++ b/app/soapbox/entity-store/hooks/useEntityLookup.ts
@@ -0,0 +1,66 @@
+import { useEffect } from 'react';
+import { z } from 'zod';
+
+import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks';
+import { type RootState } from 'soapbox/store';
+
+import { importEntities } from '../actions';
+import { Entity } from '../types';
+
+import { EntityFn } from './types';
+import { type UseEntityOpts } from './useEntity';
+
+/** Entities will be filtered through this function until it returns true. */
+type LookupFn = (entity: TEntity) => boolean
+
+function useEntityLookup(
+ entityType: string,
+ lookupFn: LookupFn,
+ entityFn: EntityFn,
+ opts: UseEntityOpts = {},
+) {
+ const { schema = z.custom() } = opts;
+
+ const dispatch = useAppDispatch();
+ const [isFetching, setPromise] = useLoading(true);
+
+ const entity = useAppSelector(state => findEntity(state, entityType, lookupFn));
+ const isLoading = isFetching && !entity;
+
+ const fetchEntity = async () => {
+ try {
+ const response = await setPromise(entityFn());
+ const entity = schema.parse(response.data);
+ dispatch(importEntities([entity], entityType));
+ } catch (e) {
+ // do nothing
+ }
+ };
+
+ useEffect(() => {
+ if (!entity || opts.refetch) {
+ fetchEntity();
+ }
+ }, []);
+
+ return {
+ entity,
+ fetchEntity,
+ isFetching,
+ isLoading,
+ };
+}
+
+function findEntity(
+ state: RootState,
+ entityType: string,
+ lookupFn: LookupFn,
+) {
+ const cache = state.entities[entityType];
+
+ if (cache) {
+ return (Object.values(cache.store) as TEntity[]).find(lookupFn);
+ }
+}
+
+export { useEntityLookup };
\ No newline at end of file
diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx
index 96f807f9d..96653f97e 100644
--- a/app/soapbox/features/group/components/group-action-button.tsx
+++ b/app/soapbox/features/group/components/group-action-button.tsx
@@ -82,7 +82,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
return (
diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx
index 7527a1802..4176f6106 100644
--- a/app/soapbox/features/group/edit-group.tsx
+++ b/app/soapbox/features/group/edit-group.tsx
@@ -26,11 +26,11 @@ const messages = defineMessages({
interface IEditGroup {
params: {
- id: string
+ groupId: string
}
}
-const EditGroup: React.FC = ({ params: { id: groupId } }) => {
+const EditGroup: React.FC = ({ params: { groupId } }) => {
const intl = useIntl();
const instance = useInstance();
diff --git a/app/soapbox/features/group/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx
index a82f889c1..988b90b1b 100644
--- a/app/soapbox/features/group/group-blocked-members.tsx
+++ b/app/soapbox/features/group/group-blocked-members.tsx
@@ -12,7 +12,7 @@ import toast from 'soapbox/toast';
import ColumnForbidden from '../ui/components/column-forbidden';
-type RouteParams = { id: string };
+type RouteParams = { groupId: string };
const messages = defineMessages({
heading: { id: 'column.group_blocked_members', defaultMessage: 'Banned Members' },
@@ -62,7 +62,7 @@ const GroupBlockedMembers: React.FC = ({ params }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
- const id = params?.id;
+ const id = params?.groupId;
const { group } = useGroup(id);
const accountIds = useAppSelector((state) => state.user_lists.group_blocks.get(id)?.items);
@@ -86,7 +86,7 @@ const GroupBlockedMembers: React.FC = ({ params }) => {
const emptyMessage = ;
return (
-
+
{
const dispatch = useAppDispatch();
- const { id: groupId } = useParams<{ id: string }>();
+ const { groupId } = useParams<{ groupId: string }>();
const { group, isLoading: groupIsLoading } = useGroup(groupId);
diff --git a/app/soapbox/features/group/group-members.tsx b/app/soapbox/features/group/group-members.tsx
index a3a790a62..36ccd05b7 100644
--- a/app/soapbox/features/group/group-members.tsx
+++ b/app/soapbox/features/group/group-members.tsx
@@ -16,13 +16,13 @@ import GroupMemberListItem from './components/group-member-list-item';
import type { Group } from 'soapbox/types/entities';
interface IGroupMembers {
- params: { id: string }
+ params: { groupId: string }
}
export const MAX_ADMIN_COUNT = 5;
const GroupMembers: React.FC = (props) => {
- const groupId = props.params.id;
+ const { groupId } = props.params;
const features = useFeatures();
@@ -58,7 +58,10 @@ const GroupMembers: React.FC = (props) => {
itemClassName='py-3 last:pb-0'
prepend={(pendingCount > 0) && (
)}
>
diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx
index dc1190bbf..c83d45b06 100644
--- a/app/soapbox/features/group/group-membership-requests.tsx
+++ b/app/soapbox/features/group/group-membership-requests.tsx
@@ -14,7 +14,7 @@ import ColumnForbidden from '../ui/components/column-forbidden';
import type { Account as AccountEntity } from 'soapbox/schemas';
-type RouteParams = { id: string };
+type RouteParams = { groupId: string };
const messages = defineMessages({
heading: { id: 'column.group_pending_requests', defaultMessage: 'Pending requests' },
@@ -54,7 +54,7 @@ interface IGroupMembershipRequests {
}
const GroupMembershipRequests: React.FC = ({ params }) => {
- const id = params?.id;
+ const id = params?.groupId;
const intl = useIntl();
const { group } = useGroup(id);
diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx
index d0b371d8b..516fb94df 100644
--- a/app/soapbox/features/group/group-tags.tsx
+++ b/app/soapbox/features/group/group-tags.tsx
@@ -13,11 +13,11 @@ import GroupTagListItem from './components/group-tag-list-item';
import type { Group } from 'soapbox/types/entities';
interface IGroupTopics {
- params: { id: string }
+ params: { groupId: string }
}
const GroupTopics: React.FC = (props) => {
- const groupId = props.params.id;
+ const { groupId } = props.params;
const { group, isFetching: isFetchingGroup } = useGroup(groupId);
const { tags, isFetching: isFetchingTags, hasNextPage, fetchNextPage } = useGroupTags(groupId);
diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx
index f19da1777..53f570280 100644
--- a/app/soapbox/features/group/group-timeline.tsx
+++ b/app/soapbox/features/group/group-timeline.tsx
@@ -12,7 +12,7 @@ import { useGroup } from 'soapbox/hooks/api';
import Timeline from '../ui/components/timeline';
-type RouteParams = { id: string };
+type RouteParams = { groupId: string };
interface IGroupTimeline {
params: RouteParams
@@ -22,7 +22,7 @@ const GroupTimeline: React.FC = (props) => {
const account = useOwnAccount();
const dispatch = useAppDispatch();
- const groupId = props.params.id;
+ const { groupId } = props.params;
const { group } = useGroup(groupId);
diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx
index e8bed3f30..5c3ee5c24 100644
--- a/app/soapbox/features/group/manage-group.tsx
+++ b/app/soapbox/features/group/manage-group.tsx
@@ -13,7 +13,7 @@ import { TRUTHSOCIAL } from 'soapbox/utils/features';
import ColumnForbidden from '../ui/components/column-forbidden';
-type RouteParams = { id: string };
+type RouteParams = { groupId: string };
const messages = defineMessages({
heading: { id: 'column.manage_group', defaultMessage: 'Manage group' },
@@ -34,7 +34,7 @@ interface IManageGroup {
}
const ManageGroup: React.FC = ({ params }) => {
- const { id } = params;
+ const { groupId: id } = params;
const backend = useBackend();
const dispatch = useAppDispatch();
@@ -76,12 +76,12 @@ const ManageGroup: React.FC = ({ params }) => {
},
}));
- const navigateToEdit = () => history.push(`/groups/${id}/manage/edit`);
- const navigateToPending = () => history.push(`/groups/${id}/manage/requests`);
- const navigateToBlocks = () => history.push(`/groups/${id}/manage/blocks`);
+ const navigateToEdit = () => history.push(`/group/${group.slug}/manage/edit`);
+ const navigateToPending = () => history.push(`/group/${group.slug}/manage/requests`);
+ const navigateToBlocks = () => history.push(`/group/${group.slug}/manage/blocks`);
return (
-
+
{isOwner && (
<>
diff --git a/app/soapbox/features/groups/components/discover/group-grid-item.tsx b/app/soapbox/features/groups/components/discover/group-grid-item.tsx
index 17d53225e..fd1168f4e 100644
--- a/app/soapbox/features/groups/components/discover/group-grid-item.tsx
+++ b/app/soapbox/features/groups/components/discover/group-grid-item.tsx
@@ -25,7 +25,7 @@ const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef
-
+
{
alignItems='center'
justifyContent='between'
>
-
+
= ({ card }) => {
const { group } = card;
if (!group) return null;
- const navigateToGroup = () => history.push(`/groups/${group.id}`);
+ const navigateToGroup = () => history.push(`/group/${group.slug}`);
return (
diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx
index 2f6436cd5..0407ff8e0 100644
--- a/app/soapbox/features/groups/index.tsx
+++ b/app/soapbox/features/groups/index.tsx
@@ -106,7 +106,7 @@ const Groups: React.FC = () => {
placeholderCount={3}
>
{groups.map((group) => (
-
+
))}
diff --git a/app/soapbox/features/groups/pending-requests.tsx b/app/soapbox/features/groups/pending-requests.tsx
index cc9ceed1d..1233ff3a7 100644
--- a/app/soapbox/features/groups/pending-requests.tsx
+++ b/app/soapbox/features/groups/pending-requests.tsx
@@ -57,7 +57,7 @@ export default () => {
showLoading={isLoading && groups.length === 0}
>
{groups.map((group) => (
-
+
))}
diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx
index 28bde468b..1e90ed415 100644
--- a/app/soapbox/features/status/index.tsx
+++ b/app/soapbox/features/status/index.tsx
@@ -118,6 +118,7 @@ type DisplayMedia = 'default' | 'hide_all' | 'show_all';
type RouteParams = {
statusId: string
groupId?: string
+ groupSlug?: string
};
interface IThread {
@@ -516,8 +517,10 @@ const Thread: React.FC = (props) => {
children.push(...renderChildren(descendantsIds).toArray());
}
- if (status.group && typeof status.group === 'object' && !props.params.groupId) {
- return ;
+ if (status.group && typeof status.group === 'object') {
+ if (status.group.slug && !props.params.groupSlug) {
+ return ;
+ }
}
const titleMessage = () => {
diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx
index bcd6d8d24..565666465 100644
--- a/app/soapbox/features/ui/index.tsx
+++ b/app/soapbox/features/ui/index.tsx
@@ -20,6 +20,8 @@ import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
import { connectUserStream } from 'soapbox/actions/streaming';
import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions';
import { expandHomeTimeline } from 'soapbox/actions/timelines';
+import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc';
+import withHoc from 'soapbox/components/hoc/with-hoc';
import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb-navigation';
import { Layout } from 'soapbox/components/ui';
@@ -141,6 +143,16 @@ import { WrappedRoute } from './util/react-router-helpers';
// Without this it ends up in ~8 very commonly used bundles.
import 'soapbox/components/status';
+const GroupTagsSlug = withHoc(GroupTags as any, GroupLookupHoc);
+const GroupTagTimelineSlug = withHoc(GroupTagTimeline as any, GroupLookupHoc);
+const GroupTimelineSlug = withHoc(GroupTimeline as any, GroupLookupHoc);
+const GroupMembersSlug = withHoc(GroupMembers as any, GroupLookupHoc);
+const GroupGallerySlug = withHoc(GroupGallery as any, GroupLookupHoc);
+const ManageGroupSlug = withHoc(ManageGroup as any, GroupLookupHoc);
+const EditGroupSlug = withHoc(EditGroup as any, GroupLookupHoc);
+const GroupBlockedMembersSlug = withHoc(GroupBlockedMembers as any, GroupLookupHoc);
+const GroupMembershipRequestsSlug = withHoc(GroupMembershipRequests as any, GroupLookupHoc);
+
const EmptyPage = HomePage;
const keyMap = {
@@ -303,17 +315,28 @@ const SwitchingColumnsArea: React.FC = ({ children }) =>
{features.groupsDiscovery && }
{features.groupsDiscovery && }
{features.groupsPending && }
- {features.groupsTags && }
+ {features.groupsTags && }
{features.groupsTags && }
- {features.groups && }
- {features.groups && }
- {features.groups && }
- {features.groups && }
- {features.groups && }
- {features.groups && }
- {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
{features.groups && }
+ {features.groupsTags && }
+ {features.groupsTags && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+ {features.groups && }
+
{features.scheduledStatuses && }
diff --git a/app/soapbox/hooks/api/groups/useGroupLookup.ts b/app/soapbox/hooks/api/groups/useGroupLookup.ts
new file mode 100644
index 000000000..89c778a15
--- /dev/null
+++ b/app/soapbox/hooks/api/groups/useGroupLookup.ts
@@ -0,0 +1,17 @@
+import { Entities } from 'soapbox/entity-store/entities';
+import { useEntityLookup } from 'soapbox/entity-store/hooks';
+import { useApi } from 'soapbox/hooks/useApi';
+import { groupSchema } from 'soapbox/schemas';
+
+function useGroupLookup(slug: string) {
+ const api = useApi();
+
+ return useEntityLookup(
+ Entities.GROUPS,
+ (group) => group.slug === slug,
+ () => api.get(`/api/v1/groups/lookup?name=${slug}`),
+ { schema: groupSchema },
+ );
+}
+
+export { useGroupLookup };
\ No newline at end of file
diff --git a/app/soapbox/normalizers/group.ts b/app/soapbox/normalizers/group.ts
index 44b2a48d2..127bca29f 100644
--- a/app/soapbox/normalizers/group.ts
+++ b/app/soapbox/normalizers/group.ts
@@ -33,6 +33,7 @@ export const GroupRecord = ImmutableRecord({
members_count: 0,
note: '',
statuses_visibility: 'public',
+ slug: '',
tags: [],
uri: '',
url: '',
diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx
index 07b9eb4d5..ae383be8c 100644
--- a/app/soapbox/pages/group-page.tsx
+++ b/app/soapbox/pages/group-page.tsx
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useRouteMatch } from 'react-router-dom';
+import GroupLookupHoc from 'soapbox/components/hoc/group-lookup-hoc';
import { Column, Icon, Layout, Stack, Text } from 'soapbox/components/ui';
import GroupHeader from 'soapbox/features/group/components/group-header';
import LinkFooter from 'soapbox/features/ui/components/link-footer';
@@ -28,7 +29,7 @@ const messages = defineMessages({
interface IGroupPage {
params?: {
- id?: string
+ groupId?: string
}
children: React.ReactNode
}
@@ -66,7 +67,7 @@ const GroupPage: React.FC = ({ params, children }) => {
const match = useRouteMatch();
const me = useOwnAccount();
- const id = params?.id || '';
+ const id = params?.groupId || '';
const { group } = useGroup(id);
const { accounts: pending } = useGroupMembershipRequests(id);
@@ -85,28 +86,28 @@ const GroupPage: React.FC = ({ params, children }) => {
const items = [];
items.push({
text: intl.formatMessage(messages.all),
- to: `/groups/${group?.id}`,
- name: '/groups/:id',
+ to: `/group/${group?.slug}`,
+ name: '/group/:groupSlug',
});
if (features.groupsTags) {
items.push({
text: intl.formatMessage(messages.tags),
- to: `/groups/${group?.id}/tags`,
- name: '/groups/:id/tags',
+ to: `/group/${group?.slug}/tags`,
+ name: '/group/:groupSlug/tags',
});
}
items.push({
text: intl.formatMessage(messages.members),
- to: `/groups/${group?.id}/members`,
- name: '/groups/:id/members',
+ to: `/group/${group?.slug}/members`,
+ name: '/group/:groupSlug/members',
count: pending.length,
},
{
text: intl.formatMessage(messages.media),
- to: `/groups/${group?.id}/media`,
- name: '/groups/:id/media',
+ to: `/group/${group?.slug}/media`,
+ name: '/group/:groupSlug/media',
});
return items;
@@ -161,4 +162,4 @@ const GroupPage: React.FC = ({ params, children }) => {
);
};
-export default GroupPage;
+export default GroupLookupHoc(GroupPage as any) as any;
diff --git a/app/soapbox/schemas/group.ts b/app/soapbox/schemas/group.ts
index cd62f5860..827531ee7 100644
--- a/app/soapbox/schemas/group.ts
+++ b/app/soapbox/schemas/group.ts
@@ -28,6 +28,7 @@ const groupSchema = z.object({
members_count: z.number().catch(0),
note: z.string().transform(note => note === '' ? '' : note).catch(''),
relationship: groupRelationshipSchema.nullable().catch(null), // Dummy field to be overwritten later
+ slug: z.string().catch(''), // TruthSocial
statuses_visibility: z.string().catch('public'),
tags: z.array(groupTagSchema).catch([]),
uri: z.string().catch(''),