From 21419f23d4c514e59e74f6a533e8473dfa19dc52 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 12 Apr 2023 09:44:29 -0400 Subject: [PATCH 01/24] Add group context to details screen --- .../status/components/detailed-status.tsx | 41 ++++++++++++++++--- app/soapbox/features/status/index.tsx | 3 +- app/soapbox/locales/en.json | 1 - app/soapbox/locales/zh-CN.json | 1 - 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/app/soapbox/features/status/components/detailed-status.tsx b/app/soapbox/features/status/components/detailed-status.tsx index 4fc0f0eaf..05e435d4e 100644 --- a/app/soapbox/features/status/components/detailed-status.tsx +++ b/app/soapbox/features/status/components/detailed-status.tsx @@ -2,20 +2,20 @@ import React, { useEffect, useRef, useState } from 'react'; import { FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import Account from 'soapbox/components/account'; -import Icon from 'soapbox/components/icon'; import StatusContent from 'soapbox/components/status-content'; import StatusMedia from 'soapbox/components/status-media'; import StatusReplyMentions from 'soapbox/components/status-reply-mentions'; import SensitiveContentOverlay from 'soapbox/components/statuses/sensitive-content-overlay'; +import StatusInfo from 'soapbox/components/statuses/status-info'; import TranslateButton from 'soapbox/components/translate-button'; -import { HStack, Stack, Text } from 'soapbox/components/ui'; +import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; import QuotedStatus from 'soapbox/features/status/containers/quoted-status-container'; import { getActualStatus } from 'soapbox/utils/status'; import StatusInteractionBar from './status-interaction-bar'; import type { List as ImmutableList } from 'immutable'; -import type { Attachment as AttachmentEntity, Status as StatusEntity } from 'soapbox/types/entities'; +import type { Attachment as AttachmentEntity, Group, Status as StatusEntity } from 'soapbox/types/entities'; interface IDetailedStatus { status: StatusEntity @@ -50,6 +50,35 @@ const DetailedStatus: React.FC = ({ onOpenCompareHistoryModal(status); }; + const renderStatusInfo = () => { + if (status.group) { + return ( +
+ } + text={ + + + ) }} + /> + + } + /> +
+ ); + } + }; + const actualStatus = getActualStatus(status); if (!actualStatus) return null; const { account } = actualStatus; @@ -75,14 +104,16 @@ const DetailedStatus: React.FC = ({ } if (actualStatus.visibility === 'direct') { - statusTypeIcon = ; + statusTypeIcon = ; } else if (actualStatus.visibility === 'private') { - statusTypeIcon = ; + statusTypeIcon = ; } return (
+ {renderStatusInfo()} +
= (props) => { const titleMessage = () => { if (status.visibility === 'direct') return messages.titleDirect; - return status.group ? messages.titleGroup : messages.title; + return messages.title; }; return ( diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 265cb1bcc..002cbe6c8 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -1465,7 +1465,6 @@ "status.show_original": "Show original", "status.title": "Post Details", "status.title_direct": "Direct message", - "status.title_group": "Group Post Details", "status.translate": "Translate", "status.translated_from_with": "Translated from {lang} using {provider}", "status.unbookmark": "Remove bookmark", diff --git a/app/soapbox/locales/zh-CN.json b/app/soapbox/locales/zh-CN.json index 617c8e727..889752971 100644 --- a/app/soapbox/locales/zh-CN.json +++ b/app/soapbox/locales/zh-CN.json @@ -1464,7 +1464,6 @@ "status.show_original": "显示原文本", "status.title": "帖文详情", "status.title_direct": "私信", - "status.title_group": "群组帖文详情", "status.translate": "翻译", "status.translated_from_with": "使用 {provider} 从 {lang} 翻译而来", "status.unbookmark": "移除书签", From 17757ea326649e710bff04d5ec95f8d0047f6a39 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Apr 2023 15:39:41 -0500 Subject: [PATCH 02/24] Add useEntityLookup hook --- app/soapbox/entity-store/hooks/useEntity.ts | 1 + .../entity-store/hooks/useEntityLookup.ts | 66 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 app/soapbox/entity-store/hooks/useEntityLookup.ts 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..ab0ea0b41 --- /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 default useEntityLookup; \ No newline at end of file From 4e822a80dd66fb4b616ce3d3a36d1cc32c5ca59f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 12 Apr 2023 15:51:30 -0500 Subject: [PATCH 03/24] Add useGroupLookup hook --- app/soapbox/entity-store/hooks/index.ts | 1 + .../entity-store/hooks/useEntityLookup.ts | 2 +- app/soapbox/hooks/api/groups/useGroupLookup.ts | 17 +++++++++++++++++ app/soapbox/schemas/group.ts | 1 + 4 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/hooks/api/groups/useGroupLookup.ts 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/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts index ab0ea0b41..a49a659a4 100644 --- a/app/soapbox/entity-store/hooks/useEntityLookup.ts +++ b/app/soapbox/entity-store/hooks/useEntityLookup.ts @@ -63,4 +63,4 @@ function findEntity( } } -export default useEntityLookup; \ No newline at end of file +export { useEntityLookup }; \ No newline at end of file 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/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(''), From 5d29666b41707658f853f09945023773dce006a4 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 29 Mar 2023 13:21:28 -0400 Subject: [PATCH 04/24] Add support for trending tags --- app/soapbox/entity-store/entities.ts | 5 +- .../components/discover/popular-tags.tsx | 52 ++++++++ .../components/discover/tag-list-item.tsx | 39 ++++++ app/soapbox/features/groups/discover.tsx | 2 + app/soapbox/features/groups/tags.tsx | 112 ++++++++++++++++++ app/soapbox/features/ui/index.tsx | 2 + .../features/ui/util/async-components.ts | 4 + .../hooks/api/groups/useGroupsFromTag.ts | 29 +++++ .../hooks/api/groups/usePopularTags.ts | 27 +++++ app/soapbox/hooks/api/index.ts | 2 + app/soapbox/schemas/group-tag.ts | 7 +- app/soapbox/schemas/index.ts | 2 + 12 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/features/groups/components/discover/popular-tags.tsx create mode 100644 app/soapbox/features/groups/components/discover/tag-list-item.tsx create mode 100644 app/soapbox/features/groups/tags.tsx create mode 100644 app/soapbox/hooks/api/groups/useGroupsFromTag.ts create mode 100644 app/soapbox/hooks/api/groups/usePopularTags.ts diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index 719cc7f1c..9878cbbf2 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -1,8 +1,9 @@ export enum Entities { ACCOUNTS = 'Accounts', GROUPS = 'Groups', - GROUP_RELATIONSHIPS = 'GroupRelationships', GROUP_MEMBERSHIPS = 'GroupMemberships', + GROUP_RELATIONSHIPS = 'GroupRelationships', + GROUP_TAGS = 'GroupTags', RELATIONSHIPS = 'Relationships', - STATUSES = 'Statuses', + STATUSES = 'Statuses' } \ No newline at end of file diff --git a/app/soapbox/features/groups/components/discover/popular-tags.tsx b/app/soapbox/features/groups/components/discover/popular-tags.tsx new file mode 100644 index 000000000..3b0296191 --- /dev/null +++ b/app/soapbox/features/groups/components/discover/popular-tags.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Link from 'soapbox/components/link'; +import { HStack, Stack, Text } from 'soapbox/components/ui'; +import { usePopularTags } from 'soapbox/hooks/api'; + +import TagListItem from './tag-list-item'; + +const PopularTags = () => { + const { tags, isFetched, isError } = usePopularTags(); + const isEmpty = (isFetched && tags.length === 0) || isError; + + return ( + + + + + + + + + + + + + + {isEmpty ? ( + + + + ) : ( + + {tags.slice(0, 10).map((tag) => ( + + ))} + + )} + + ); +}; + +export default PopularTags; \ No newline at end of file diff --git a/app/soapbox/features/groups/components/discover/tag-list-item.tsx b/app/soapbox/features/groups/components/discover/tag-list-item.tsx new file mode 100644 index 000000000..c899e1085 --- /dev/null +++ b/app/soapbox/features/groups/components/discover/tag-list-item.tsx @@ -0,0 +1,39 @@ +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 ( + + + + #{tag.name} + + + + + :{' '} + {tag.uses} + + + + ); +}; + +export default TagListItem; \ No newline at end of file diff --git a/app/soapbox/features/groups/discover.tsx b/app/soapbox/features/groups/discover.tsx index 4e0c0c70a..6e26c0671 100644 --- a/app/soapbox/features/groups/discover.tsx +++ b/app/soapbox/features/groups/discover.tsx @@ -4,6 +4,7 @@ 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'; @@ -71,6 +72,7 @@ const Discover: React.FC = () => { <> + )} diff --git a/app/soapbox/features/groups/tags.tsx b/app/soapbox/features/groups/tags.tsx new file mode 100644 index 000000000..052f5a6e3 --- /dev/null +++ b/app/soapbox/features/groups/tags.tsx @@ -0,0 +1,112 @@ +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 { 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 ITags { + params: { id: string } +} + +const Tags: React.FC = (props) => { + const tagId = props.params.id; + + const [layout, setLayout] = useState(Layout.LIST); + + 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) => ( +
+ +
+ ), []); + + return ( + + + + + + } + > + {layout === Layout.LIST ? ( + renderGroupList(group, index)} + endReached={handleLoadMore} + /> + ) : ( + renderGroupGrid(group, index)} + components={{ + Item: (props) => ( +
+ ), + List: GridList, + }} + endReached={handleLoadMore} + /> + )} + + ); +}; + +export default Tags; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index b58f266ec..15cdc8033 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -122,6 +122,7 @@ import { GroupsDiscover, GroupsPopular, GroupsSuggested, + GroupsTags, PendingGroupRequests, GroupMembers, GroupTimeline, @@ -296,6 +297,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {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 f915571d2..01e44ca0a 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 GroupsTags() { + return import(/* webpackChunkName: "features/groups" */'../../groups/tags'); +} + export function PendingGroupRequests() { return import(/* webpackChunkName: "features/groups" */'../../groups/pending-requests'); } diff --git a/app/soapbox/hooks/api/groups/useGroupsFromTag.ts b/app/soapbox/hooks/api/groups/useGroupsFromTag.ts new file mode 100644 index 000000000..7ebf871f4 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupsFromTag.ts @@ -0,0 +1,29 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { groupSchema } from 'soapbox/schemas'; + +import { useApi } from '../../useApi'; +import { useFeatures } from '../../useFeatures'; + +import type { Group } from 'soapbox/schemas'; + +function useGroupsFromTag(tagId: string) { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'tags', tagId], + () => api.get(`/api/mock/tags/${tagId}/groups`), + { + schema: groupSchema, + enabled: features.groupsDiscovery, + }, + ); + + return { + ...result, + groups: entities, + }; +} + +export { useGroupsFromTag }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/usePopularTags.ts b/app/soapbox/hooks/api/groups/usePopularTags.ts new file mode 100644 index 000000000..d6fe5e0af --- /dev/null +++ b/app/soapbox/hooks/api/groups/usePopularTags.ts @@ -0,0 +1,27 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { GroupTag, groupTagSchema } from 'soapbox/schemas'; + +import { useApi } from '../../useApi'; +import { useFeatures } from '../../useFeatures'; + +function usePopularTags() { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUP_TAGS], + () => api.get('/api/mock/groups/tags'), + { + schema: groupTagSchema, + enabled: features.groupsDiscovery, + }, + ); + + return { + ...result, + tags: entities, + }; +} + +export { usePopularTags }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index b5927bf6b..f28c6a5a5 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -16,8 +16,10 @@ export { useGroup, useGroups } from './groups/useGroups'; export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; export { useGroupSearch } from './groups/useGroupSearch'; export { useGroupValidation } from './groups/useGroupValidation'; +export { useGroupsFromTag } from './groups/useGroupsFromTag'; export { useJoinGroup } from './groups/useJoinGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; +export { usePopularTags } from './groups/usePopularTags'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; export { useUpdateGroup } from './groups/useUpdateGroup'; diff --git a/app/soapbox/schemas/group-tag.ts b/app/soapbox/schemas/group-tag.ts index cc64deec6..4bd1ee66c 100644 --- a/app/soapbox/schemas/group-tag.ts +++ b/app/soapbox/schemas/group-tag.ts @@ -1,7 +1,12 @@ -import { z } from 'zod'; +import z from 'zod'; const groupTagSchema = z.object({ + id: z.string(), + uses: z.number(), name: z.string(), + url: z.string(), + pinned: z.boolean().catch(false), + visible: z.boolean().default(true), }); type GroupTag = z.infer; diff --git a/app/soapbox/schemas/index.ts b/app/soapbox/schemas/index.ts index 1eed8905b..a675b52d2 100644 --- a/app/soapbox/schemas/index.ts +++ b/app/soapbox/schemas/index.ts @@ -6,6 +6,7 @@ export { customEmojiSchema } from './custom-emoji'; export { groupSchema } from './group'; export { groupMemberSchema } from './group-member'; export { groupRelationshipSchema } from './group-relationship'; +export { groupTagSchema } from './group-tag'; export { relationshipSchema } from './relationship'; /** @@ -16,4 +17,5 @@ export type { CustomEmoji } from './custom-emoji'; export type { Group } from './group'; export type { GroupMember } from './group-member'; export type { GroupRelationship } from './group-relationship'; +export type { GroupTag } from './group-tag'; export type { Relationship } from './relationship'; From 8702c169988461a62a3673845fa14e2f902c2729 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 13 Apr 2023 07:34:37 -0400 Subject: [PATCH 05/24] Update GroupTag schema --- app/soapbox/schemas/group-tag.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/schemas/group-tag.ts b/app/soapbox/schemas/group-tag.ts index 4bd1ee66c..9fa885569 100644 --- a/app/soapbox/schemas/group-tag.ts +++ b/app/soapbox/schemas/group-tag.ts @@ -2,11 +2,11 @@ import z from 'zod'; const groupTagSchema = z.object({ id: z.string(), - uses: z.number(), name: z.string(), - url: z.string(), - pinned: z.boolean().catch(false), - visible: z.boolean().default(true), + uses: z.number().optional(), + url: z.string().optional(), + pinned: z.boolean().optional().catch(false), + visible: z.boolean().optional().default(true), }); type GroupTag = z.infer; From bb70c1ea3c6b36d100e442149a2853fc35112a64 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 13 Apr 2023 09:28:40 -0400 Subject: [PATCH 06/24] Add topic pages --- app/soapbox/features/groups/tag.tsx | 117 ++++++++++++++++++ app/soapbox/features/groups/tags.tsx | 104 ++++------------ app/soapbox/features/ui/index.tsx | 4 +- .../features/ui/util/async-components.ts | 4 + app/soapbox/hooks/api/groups/useGroupTag.ts | 21 ++++ .../hooks/api/groups/useGroupsFromTag.ts | 12 +- .../hooks/api/groups/usePopularTags.ts | 2 +- app/soapbox/hooks/api/index.ts | 1 + app/soapbox/locales/en.json | 5 + 9 files changed, 189 insertions(+), 81 deletions(-) create mode 100644 app/soapbox/features/groups/tag.tsx create mode 100644 app/soapbox/hooks/api/groups/useGroupTag.ts 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}", From 4cd43c340b62882f931815196a62585cf735c988 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 13 Apr 2023 10:43:24 -0500 Subject: [PATCH 07/24] Fix group types :( --- app/soapbox/normalizers/group.ts | 1 + 1 file changed, 1 insertion(+) 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: '', From 6da45c3ef7bef11c93e091ba45891f0783cc03e0 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 23 Mar 2023 08:43:41 -0400 Subject: [PATCH 08/24] Add ability to search my Groups --- app/soapbox/features/group/manage-group.tsx | 3 +-- app/soapbox/hooks/__tests__/useGroupsPath.test.ts | 2 +- app/soapbox/hooks/api/index.ts | 2 +- app/soapbox/hooks/index.ts | 1 + 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 12dc0b11b..e8bed3f30 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -5,9 +5,8 @@ import { useHistory } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; 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 { useBackend } from 'soapbox/hooks/useBackend'; import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; import { TRUTHSOCIAL } from 'soapbox/utils/features'; diff --git a/app/soapbox/hooks/__tests__/useGroupsPath.test.ts b/app/soapbox/hooks/__tests__/useGroupsPath.test.ts index 7596acd9a..d102fe412 100644 --- a/app/soapbox/hooks/__tests__/useGroupsPath.test.ts +++ b/app/soapbox/hooks/__tests__/useGroupsPath.test.ts @@ -53,7 +53,7 @@ describe('useGroupsPath()', () => { describe('when the user has groups', () => { beforeEach(() => { __stub((mock) => { - mock.onGet('/api/v1/groups').reply(200, [ + mock.onGet('/api/v1/groups?q=').reply(200, [ buildGroup({ display_name: 'Group', id: '1', diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index 800ea5857..3ab7be9a5 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -11,8 +11,8 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest' export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup'; export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; -export { useGroupMedia } from './groups/useGroupMedia'; export { useGroup, useGroups } from './groups/useGroups'; +export { useGroupMedia } from './groups/useGroupMedia'; export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; export { useGroupSearch } from './groups/useGroupSearch'; export { useGroupTag } from './groups/useGroupTag'; diff --git a/app/soapbox/hooks/index.ts b/app/soapbox/hooks/index.ts index ef8b6462f..6f52e0c8f 100644 --- a/app/soapbox/hooks/index.ts +++ b/app/soapbox/hooks/index.ts @@ -2,6 +2,7 @@ export { useAccount } from './useAccount'; export { useApi } from './useApi'; export { useAppDispatch } from './useAppDispatch'; export { useAppSelector } from './useAppSelector'; +export { useBackend } from './useBackend'; export { useClickOutside } from './useClickOutside'; export { useCompose } from './useCompose'; export { useDebounce } from './useDebounce'; From 8ec8d4a2cae1b175650632daab013190aa65c2a9 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 23 Mar 2023 15:19:18 -0400 Subject: [PATCH 09/24] Use FloatingUI with Tooltip --- app/soapbox/components/ui/tooltip/tooltip.css | 12 -- app/soapbox/components/ui/tooltip/tooltip.tsx | 119 ++++++++++-------- package.json | 1 - yarn.lock | 19 --- 4 files changed, 70 insertions(+), 81 deletions(-) delete mode 100644 app/soapbox/components/ui/tooltip/tooltip.css diff --git a/app/soapbox/components/ui/tooltip/tooltip.css b/app/soapbox/components/ui/tooltip/tooltip.css deleted file mode 100644 index 670571c8b..000000000 --- a/app/soapbox/components/ui/tooltip/tooltip.css +++ /dev/null @@ -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; -} diff --git a/app/soapbox/components/ui/tooltip/tooltip.tsx b/app/soapbox/components/ui/tooltip/tooltip.tsx index f2b6ffedc..0d492bcb6 100644 --- a/app/soapbox/components/ui/tooltip/tooltip.tsx +++ b/app/soapbox/components/ui/tooltip/tooltip.tsx @@ -1,67 +1,88 @@ -import { TooltipPopup, useTooltip } from '@reach/tooltip'; -import React from 'react'; - -import Portal from '../portal/portal'; - -import './tooltip.css'; +import { + arrow, + FloatingArrow, + FloatingPortal, + offset, + useFloating, + useHover, + useInteractions, + useTransitionStyles, +} from '@floating-ui/react'; +import React, { useRef, useState } from 'react'; interface ITooltip { + /** Element to display the tooltip around. */ + children: React.ReactElement> /** Text to display in the tooltip. */ text: string - /** Element to display the tooltip around. */ - children: React.ReactNode } -const centered = (triggerRect: any, tooltipRect: any) => { - const triggerCenter = triggerRect.left + triggerRect.width / 2; - const left = triggerCenter - tooltipRect.width / 2; - const maxLeft = window.innerWidth - tooltipRect.width - 2; - return { - left: Math.min(Math.max(2, left), maxLeft) + window.scrollX, - top: triggerRect.bottom + 8 + window.scrollY, - }; -}; +/** + * Tooltip + */ +const Tooltip: React.FC = (props) => { + const { children, text } = props; -/** Hoverable tooltip element. */ -const Tooltip: React.FC = ({ - children, - text, -}) => { - // get the props from useTooltip - const [trigger, tooltip] = useTooltip(); + const [isOpen, setIsOpen] = useState(false); - // destructure off what we need to position the triangle - const { isVisible, triggerRect } = tooltip; + const arrowRef = useRef(null); + + 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 ( - - {React.cloneElement(children as any, trigger)} + <> + {React.cloneElement(children, { + ref: refs.setReference, + ...getReferenceProps(), + })} - {isVisible && ( - // The Triangle. We position it relative to the trigger, not the popup - // 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 - + {(isMounted) && ( +
- + 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' + {...getFloatingProps()} + > + {text} + + +
+
)} - -
+ ); }; -export default Tooltip; +export default Tooltip; \ No newline at end of file diff --git a/package.json b/package.json index 57831cd0e..ddd13f24a 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@reach/popover": "^0.18.0", "@reach/rect": "^0.18.0", "@reach/tabs": "^0.18.0", - "@reach/tooltip": "^0.18.0", "@reduxjs/toolkit": "^1.8.1", "@sentry/browser": "^7.37.2", "@sentry/react": "^7.37.2", diff --git a/yarn.lock b/yarn.lock index 4d6585981..5220b11d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2749,30 +2749,11 @@ "@reach/polymorphic" "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": version "0.18.0" resolved "https://registry.yarnpkg.com/@reach/utils/-/utils-0.18.0.tgz#4f3cebe093dd436eeaff633809bf0f68f4f9d2ee" 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": version "1.8.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.1.tgz#94ee1981b8cf9227cda40163a04704a9544c9a9f" From 2d52c8c3e48e1bdf8ebec8a8e236e0c5cf951b56 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 23 Mar 2023 15:20:19 -0400 Subject: [PATCH 10/24] Add support for Group tags --- .../entity-store/hooks/useEntityActions.ts | 3 +- .../group/components/group-tag-list-item.tsx | 152 ++++++++++++++++++ .../features/group/group-tag-timeline.tsx | 30 ++++ app/soapbox/features/group/group-tags.tsx | 54 +++++++ app/soapbox/features/ui/index.tsx | 4 + .../features/ui/util/async-components.ts | 8 + app/soapbox/hooks/api/groups/useGroupTags.ts | 20 +++ .../hooks/api/groups/useUpdateGroupTag.ts | 17 ++ app/soapbox/hooks/api/index.ts | 4 +- app/soapbox/pages/group-page.tsx | 38 +++-- app/soapbox/utils/features.ts | 5 + 11 files changed, 324 insertions(+), 11 deletions(-) create mode 100644 app/soapbox/features/group/components/group-tag-list-item.tsx create mode 100644 app/soapbox/features/group/group-tag-timeline.tsx create mode 100644 app/soapbox/features/group/group-tags.tsx create mode 100644 app/soapbox/hooks/api/groups/useGroupTags.ts create mode 100644 app/soapbox/hooks/api/groups/useUpdateGroupTag.ts diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index 8b87c52fd..ff27af340 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -12,8 +12,9 @@ interface UseEntityActionsOpts { } interface EntityActionEndpoints { - post?: string delete?: string + patch?: string + post?: string } function useEntityActions( diff --git a/app/soapbox/features/group/components/group-tag-list-item.tsx b/app/soapbox/features/group/components/group-tag-list-item.tsx new file mode 100644 index 000000000..7792b3e81 --- /dev/null +++ b/app/soapbox/features/group/components/group-tag-list-item.tsx @@ -0,0 +1,152 @@ +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 { useUpdateGroupTag } from 'soapbox/hooks/api'; +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 intl = useIntl(); + const updateGroupTag = useUpdateGroupTag(group.id, tag.id); + + const toggleVisibility = () => { + updateGroupTag({ + pinned: !tag.visible, + }, { + onSuccess(entity: GroupTag) { + toast.success( + entity.visible ? + intl.formatMessage(messages.visibleSuccess) : + intl.formatMessage(messages.hiddenSuccess), + ); + }, + }); + }; + + const togglePin = () => { + updateGroupTag({ + pinned: !tag.pinned, + }, { + onSuccess(entity: GroupTag) { + toast.success( + entity.pinned ? + intl.formatMessage(messages.pinSuccess) : + intl.formatMessage(messages.unpinSuccess), + ); + }, + }); + }; + + const renderPinIcon = () => { + if (isPinnable) { + return ( + + + + ); + } + + if (!isPinnable && tag.pinned) { + return ( + + + + + ); + } + }; + + return ( + + + + + #{tag.name} + + + {intl.formatMessage(messages.total)}: + {' '} + + {shortNumberFormat(tag.uses)} + + + + + + + {tag.visible ? ( + renderPinIcon() + ) : null} + + + + + + + ); +}; + +export default GroupTagListItem; \ No newline at end of file diff --git a/app/soapbox/features/group/group-tag-timeline.tsx b/app/soapbox/features/group/group-tag-timeline.tsx new file mode 100644 index 000000000..2663c59cf --- /dev/null +++ b/app/soapbox/features/group/group-tag-timeline.tsx @@ -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 = (props) => { + const groupId = props.params.groupId; + const tagId = props.params.id; + + const { group } = useGroup(groupId); + const { tag } = useGroupTag(tagId); + + if (!group) { + return null; + } + + return ( + + {/* TODO */} + + ); +}; + +export default GroupTagTimeline; diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx new file mode 100644 index 000000000..dc4c7d088 --- /dev/null +++ b/app/soapbox/features/group/group-tags.tsx @@ -0,0 +1,54 @@ +import React from 'react'; + +import ScrollableList from 'soapbox/components/scrollable-list'; +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 = (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 ( + <> + + {tags.map((tag) => ( + + ))} + + + ); +}; + +export default GroupTopics; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index b967f27de..bcd6d8d24 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -126,6 +126,8 @@ import { GroupsTags, PendingGroupRequests, GroupMembers, + GroupTags, + GroupTagTimeline, GroupTimeline, ManageGroup, GroupBlockedMembers, @@ -301,6 +303,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsPending && } + {features.groupsTags && } + {features.groupsTags && } {features.groups && } {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 654cab76d..e8187db46 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -578,6 +578,14 @@ export function GroupMembers() { 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() { return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline'); } diff --git a/app/soapbox/hooks/api/groups/useGroupTags.ts b/app/soapbox/hooks/api/groups/useGroupTags.ts new file mode 100644 index 000000000..4b724352e --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupTags.ts @@ -0,0 +1,20 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { groupTagSchema } from 'soapbox/schemas'; + +import type { GroupTag } from 'soapbox/schemas'; + +function useGroupTags(groupId: string) { + const { entities, ...result } = useEntities( + [Entities.GROUP_TAGS, groupId], + '/api/mock/groups/tags', // `api/v1/groups/${groupId}/tags` + { schema: groupTagSchema }, + ); + + return { + ...result, + tags: entities, + }; +} + +export { useGroupTags }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts b/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts new file mode 100644 index 000000000..2d4d61f69 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts @@ -0,0 +1,17 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { groupTagSchema } from 'soapbox/schemas'; + +import type { GroupTag } from 'soapbox/schemas'; + +function useUpdateGroupTag(groupId: string, tagId: string) { + const { updateEntity } = useEntityActions( + [Entities.GROUP_TAGS, groupId, tagId], + { patch: `/api/mock/truth/groups/${groupId}/tags/${tagId}` }, + { schema: groupTagSchema }, + ); + + return updateEntity; +} + +export { useUpdateGroupTag }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index 3ab7be9a5..c8e1f67c3 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -16,6 +16,7 @@ export { useGroupMedia } from './groups/useGroupMedia'; export { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; export { useGroupSearch } from './groups/useGroupSearch'; export { useGroupTag } from './groups/useGroupTag'; +export { useGroupTags } from './groups/useGroupTags'; export { useGroupValidation } from './groups/useGroupValidation'; export { useGroupsFromTag } from './groups/useGroupsFromTag'; export { useJoinGroup } from './groups/useJoinGroup'; @@ -23,8 +24,9 @@ export { useLeaveGroup } from './groups/useLeaveGroup'; export { usePopularTags } from './groups/usePopularTags'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; export { useUpdateGroup } from './groups/useUpdateGroup'; +export { useUpdateGroupTag } from './groups/useUpdateGroupTag'; /** * Relationships */ -export { useRelationships } from './useRelationships'; \ No newline at end of file +export { useRelationships } from './useRelationships'; diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index 449e250f3..07b9eb4d5 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useRouteMatch } from 'react-router-dom'; @@ -12,7 +12,7 @@ import { SignUpPanel, SuggestedGroupsPanel, } 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 { useGroupMembershipRequests } from 'soapbox/hooks/api/groups/useGroupMembershipRequests'; import { Group } from 'soapbox/schemas'; @@ -23,6 +23,7 @@ const messages = defineMessages({ all: { id: 'group.tabs.all', defaultMessage: 'All' }, members: { id: 'group.tabs.members', defaultMessage: 'Members' }, media: { id: 'group.tabs.media', defaultMessage: 'Media' }, + tags: { id: 'group.tabs.tags', defaultMessage: 'Topics' }, }); interface IGroupPage { @@ -61,6 +62,7 @@ const BlockedBlankslate = ({ group }: { group: Group }) => ( /** Page to display a group. */ const GroupPage: React.FC = ({ params, children }) => { const intl = useIntl(); + const features = useFeatures(); const match = useRouteMatch(); const me = useOwnAccount(); @@ -73,13 +75,29 @@ const GroupPage: React.FC = ({ params, children }) => { const isBlocked = group?.relationship?.blocked_by; const isPrivate = group?.locked; - const items = [ - { + // if ((group as any) === false) { + // return ( + // + // ); + // } + + const tabItems = useMemo(() => { + const items = []; + items.push({ text: intl.formatMessage(messages.all), to: `/groups/${group?.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), to: `/groups/${group?.id}/members`, name: '/groups/:id/members', @@ -89,8 +107,10 @@ const GroupPage: React.FC = ({ params, children }) => { text: intl.formatMessage(messages.media), to: `/groups/${group?.id}/media`, name: '/groups/:id/media', - }, - ]; + }); + + return items; + }, [features.groupsTags]); const renderChildren = () => { if (!isMember && isPrivate) { @@ -109,7 +129,7 @@ const GroupPage: React.FC = ({ params, children }) => { diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index b24e75525..9c5a68bda 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -559,6 +559,11 @@ const getInstanceFeatures = (instance: Instance) => { */ groupsSearch: v.software === TRUTHSOCIAL, + /** + * Can see topics for Groups. + */ + groupsTags: v.software === TRUTHSOCIAL, + /** * Can validate group names. */ From c5c5bd0d62c44fffd5a77f3fd217718e26d404ac Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Fri, 14 Apr 2023 15:15:34 -0400 Subject: [PATCH 11/24] Add ability to update Group tags --- .../entity-store/hooks/useEntityActions.ts | 6 +- .../group/components/group-tag-list-item.tsx | 83 ++++++++++++------- app/soapbox/features/group/group-tags.tsx | 59 ++++++++----- app/soapbox/hooks/api/groups/useGroupTags.ts | 5 +- .../hooks/api/groups/useUpdateGroupTag.ts | 11 +-- 5 files changed, 105 insertions(+), 59 deletions(-) diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index ff27af340..c7e2e431d 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -31,10 +31,14 @@ function useEntityActions( const { createEntity, isSubmitting: createSubmitting } = useCreateEntity(path, (data) => api.post(endpoints.post!, data), opts); + const { createEntity: updateEntity, isSubmitting: updateSubmitting } = + useCreateEntity(path, (data) => api.patch(endpoints.patch!, data), opts); + return { createEntity, deleteEntity, - isSubmitting: createSubmitting || deleteSubmitting, + updateEntity, + isSubmitting: createSubmitting || deleteSubmitting || updateSubmitting, }; } diff --git a/app/soapbox/features/group/components/group-tag-list-item.tsx b/app/soapbox/features/group/components/group-tag-list-item.tsx index 7792b3e81..47f9f1ef6 100644 --- a/app/soapbox/features/group/components/group-tag-list-item.tsx +++ b/app/soapbox/features/group/components/group-tag-list-item.tsx @@ -3,7 +3,11 @@ 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'; @@ -29,15 +33,26 @@ interface IGroupMemberListItem { const GroupTagListItem = (props: IGroupMemberListItem) => { const { group, tag, isPinnable } = props; + const dispatch = useAppDispatch(); const intl = useIntl(); - const updateGroupTag = useUpdateGroupTag(group.id, tag.id); + 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({ - pinned: !tag.visible, + group_tag_type: tag.visible ? 'hidden' : 'normal', }, { - onSuccess(entity: GroupTag) { + onSuccess() { + const entity = { + ...tag, + visible: !tag.visible, + }; + dispatch(importEntities([entity], Entities.GROUP_TAGS)); + toast.success( entity.visible ? intl.formatMessage(messages.visibleSuccess) : @@ -49,9 +64,15 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { const togglePin = () => { updateGroupTag({ - pinned: !tag.pinned, + group_tag_type: tag.pinned ? 'normal' : 'pinned', }, { - onSuccess(entity: GroupTag) { + onSuccess() { + const entity = { + ...tag, + pinned: !tag.pinned, + }; + dispatch(importEntities([entity], Entities.GROUP_TAGS)); + toast.success( entity.pinned ? intl.formatMessage(messages.pinSuccess) : @@ -73,7 +94,7 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { > { @@ -106,12 +127,12 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { #{tag.name} - + {intl.formatMessage(messages.total)}: {' '} @@ -121,30 +142,32 @@ const GroupTagListItem = (props: IGroupMemberListItem) => { - - {tag.visible ? ( - renderPinIcon() - ) : null} + {canEdit ? ( + + {tag.visible ? ( + renderPinIcon() + ) : null} - - - - + > + + + + ) : null} ); }; diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx index dc4c7d088..d0b371d8b 100644 --- a/app/soapbox/features/group/group-tags.tsx +++ b/app/soapbox/features/group/group-tags.tsx @@ -1,6 +1,8 @@ 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'; @@ -26,28 +28,41 @@ const GroupTopics: React.FC = (props) => { const isPinnable = pinnedTags.length < 3; return ( - <> - - {tags.map((tag) => ( - - ))} - - + +
+ +
+ + + + + + } + emptyMessageCard={false} + > + {tags.map((tag) => ( + + ))} +
); }; diff --git a/app/soapbox/hooks/api/groups/useGroupTags.ts b/app/soapbox/hooks/api/groups/useGroupTags.ts index 4b724352e..42d81688f 100644 --- a/app/soapbox/hooks/api/groups/useGroupTags.ts +++ b/app/soapbox/hooks/api/groups/useGroupTags.ts @@ -1,13 +1,16 @@ 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( [Entities.GROUP_TAGS, groupId], - '/api/mock/groups/tags', // `api/v1/groups/${groupId}/tags` + () => api.get(`api/v1/truth/trends/groups/${groupId}/tags`), { schema: groupTagSchema }, ); diff --git a/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts b/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts index 2d4d61f69..1c68c714d 100644 --- a/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts +++ b/app/soapbox/hooks/api/groups/useUpdateGroupTag.ts @@ -1,17 +1,18 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntityActions } from 'soapbox/entity-store/hooks'; -import { groupTagSchema } from 'soapbox/schemas'; import type { GroupTag } from 'soapbox/schemas'; function useUpdateGroupTag(groupId: string, tagId: string) { - const { updateEntity } = useEntityActions( + const { updateEntity, ...rest } = useEntityActions( [Entities.GROUP_TAGS, groupId, tagId], - { patch: `/api/mock/truth/groups/${groupId}/tags/${tagId}` }, - { schema: groupTagSchema }, + { patch: `/api/v1/groups/${groupId}/tags/${tagId}` }, ); - return updateEntity; + return { + updateGroupTag: updateEntity, + ...rest, + }; } export { useUpdateGroupTag }; \ No newline at end of file From ddee915d3924d07f8f68bc46ee2a59429e6d2b05 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 17 Apr 2023 10:11:03 -0400 Subject: [PATCH 12/24] Lint + Tests --- app/soapbox/hooks/__tests__/useGroupsPath.test.ts | 2 +- app/soapbox/locales/en.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/soapbox/hooks/__tests__/useGroupsPath.test.ts b/app/soapbox/hooks/__tests__/useGroupsPath.test.ts index d102fe412..7596acd9a 100644 --- a/app/soapbox/hooks/__tests__/useGroupsPath.test.ts +++ b/app/soapbox/hooks/__tests__/useGroupsPath.test.ts @@ -53,7 +53,7 @@ describe('useGroupsPath()', () => { describe('when the user has groups', () => { beforeEach(() => { __stub((mock) => { - mock.onGet('/api/v1/groups?q=').reply(200, [ + mock.onGet('/api/v1/groups').reply(200, [ buildGroup({ display_name: 'Group', id: '1', diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index e762b040c..35bfb58a0 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -809,8 +809,19 @@ "group.tabs.all": "All", "group.tabs.media": "Media", "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.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.upload_banner": "Upload photo", "groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.", From ccde5f8823d5357ec2d45875e60d949edda95391 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 17 Apr 2023 14:56:31 -0400 Subject: [PATCH 13/24] Make GroupLookup HOC kind of work --- .../components/hoc/group-lookup-hoc.tsx | 37 +++++++++++++++++++ app/soapbox/components/hoc/with-hoc.tsx | 11 ++++++ app/soapbox/features/ui/index.tsx | 5 +++ 3 files changed, 53 insertions(+) create mode 100644 app/soapbox/components/hoc/group-lookup-hoc.tsx create mode 100644 app/soapbox/components/hoc/with-hoc.tsx diff --git a/app/soapbox/components/hoc/group-lookup-hoc.tsx b/app/soapbox/components/hoc/group-lookup-hoc.tsx new file mode 100644 index 000000000..5c5f43c0c --- /dev/null +++ b/app/soapbox/components/hoc/group-lookup-hoc.tsx @@ -0,0 +1,37 @@ +import React from 'react'; + +import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup'; + +import { Spinner } from '../ui'; + +interface IGroupLookup { + params: { + groupSlug: string + } +} + +function GroupLookupHoc(Component: React.ComponentType<{ params: { groupId: string } }>) { + const GroupLookup: React.FC = (props) => { + const { entity: group } = useGroupLookup(props.params.groupSlug); + if (!group) return ( + + ); + + const newProps = { + ...props, + params: { + ...props.params, + id: group.id, + groupId: group.id, + }, + }; + + return ( + + ); + }; + + return GroupLookup; +} + +export default GroupLookupHoc; \ No newline at end of file diff --git a/app/soapbox/components/hoc/with-hoc.tsx b/app/soapbox/components/hoc/with-hoc.tsx new file mode 100644 index 000000000..d5752a45f --- /dev/null +++ b/app/soapbox/components/hoc/with-hoc.tsx @@ -0,0 +1,11 @@ +type HOC = (Component: React.ComponentType

) => React.ComponentType +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/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index b58f266ec..142db1047 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'; @@ -137,6 +139,8 @@ import { WrappedRoute } from './util/react-router-helpers'; // Without this it ends up in ~8 very commonly used bundles. import 'soapbox/components/status'; +const GroupTimelineSlug = withHoc(GroupTimeline as any, GroupLookupHoc); + const EmptyPage = HomePage; const keyMap = { @@ -305,6 +309,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groups && } {features.groups && } {features.groups && } + {features.groups && } From 1bce61182cea1a4f12984e389a510735fec56943 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 17 Apr 2023 15:28:20 -0400 Subject: [PATCH 14/24] Make components use groupId param --- .../components/hoc/group-lookup-hoc.tsx | 25 ++++++++++++++--- app/soapbox/features/group/edit-group.tsx | 4 +-- .../features/group/group-blocked-members.tsx | 4 +-- app/soapbox/features/group/group-gallery.tsx | 2 +- app/soapbox/features/group/group-members.tsx | 4 +-- .../group/group-membership-requests.tsx | 4 +-- app/soapbox/features/group/group-timeline.tsx | 4 +-- app/soapbox/features/group/manage-group.tsx | 4 +-- app/soapbox/features/ui/index.tsx | 28 ++++++++++++++----- app/soapbox/pages/group-page.tsx | 19 +++++++------ 10 files changed, 65 insertions(+), 33 deletions(-) diff --git a/app/soapbox/components/hoc/group-lookup-hoc.tsx b/app/soapbox/components/hoc/group-lookup-hoc.tsx index 5c5f43c0c..b6a125eed 100644 --- a/app/soapbox/components/hoc/group-lookup-hoc.tsx +++ b/app/soapbox/components/hoc/group-lookup-hoc.tsx @@ -1,20 +1,27 @@ import React from 'react'; +import ColumnLoading from 'soapbox/features/ui/components/column-loading'; import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup'; -import { Spinner } from '../ui'; - interface IGroupLookup { params: { groupSlug: string } } +interface IMaybeGroupLookup { + params?: { + groupSlug?: string + groupId?: string + } +} + function GroupLookupHoc(Component: React.ComponentType<{ params: { groupId: string } }>) { const GroupLookup: React.FC = (props) => { const { entity: group } = useGroupLookup(props.params.groupSlug); + if (!group) return ( - + ); const newProps = { @@ -31,7 +38,17 @@ function GroupLookupHoc(Component: React.ComponentType<{ params: { groupId: stri ); }; - return GroupLookup; + const MaybeGroupLookup: React.FC = (props) => { + const { params } = props; + + if (params?.groupId) { + return ; + } else { + return ; + } + }; + + return MaybeGroupLookup; } export default GroupLookupHoc; \ No newline at end of file diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx index 1e8024f6f..296c39e7e 100644 --- a/app/soapbox/features/group/edit-group.tsx +++ b/app/soapbox/features/group/edit-group.tsx @@ -25,11 +25,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..94b033837 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); diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx index 139be5bf3..f8a259e50 100644 --- a/app/soapbox/features/group/group-gallery.tsx +++ b/app/soapbox/features/group/group-gallery.tsx @@ -16,7 +16,7 @@ import type { Attachment, Status } from 'soapbox/types/entities'; const GroupGallery = () => { 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..fc04e3d29 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(); 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-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 12dc0b11b..38b90e34b 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -14,7 +14,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' }, @@ -35,7 +35,7 @@ interface IManageGroup { } const ManageGroup: React.FC = ({ params }) => { - const { id } = params; + const { groupId: id } = params; const backend = useBackend(); const dispatch = useAppDispatch(); diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 142db1047..c315591ac 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -140,6 +140,12 @@ import { WrappedRoute } from './util/react-router-helpers'; import 'soapbox/components/status'; 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; @@ -301,15 +307,23 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groupsDiscovery && } {features.groupsDiscovery && } {features.groupsPending && } - {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.groups && } + {features.groups && } + {features.groups && } + {features.groups && } + {features.groups && } + {features.groups && } + {features.groups && } + {features.groups && } diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index 449e250f3..61e41ad25 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -2,6 +2,7 @@ import React 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'; @@ -27,7 +28,7 @@ const messages = defineMessages({ interface IGroupPage { params?: { - id?: string + groupId?: string } children: React.ReactNode } @@ -64,7 +65,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); @@ -76,19 +77,19 @@ const GroupPage: React.FC = ({ params, children }) => { const items = [ { text: intl.formatMessage(messages.all), - to: `/groups/${group?.id}`, - name: '/groups/:id', + to: group?.slug ? `/group/${group.slug}` : `/groups/${group?.id}`, + name: group?.slug ? '/group/:groupSlug' : '/groups/:groupId', }, { text: intl.formatMessage(messages.members), - to: `/groups/${group?.id}/members`, - name: '/groups/:id/members', + to: group?.slug ? `/group/${group.slug}/members` : `/groups/${group?.id}/members`, + name: group?.slug ? '/group/:groupSlug/members' : '/groups/:groupId/members', count: pending.length, }, { text: intl.formatMessage(messages.media), - to: `/groups/${group?.id}/media`, - name: '/groups/:id/media', + to: group?.slug ? `/group/${group.slug}/media` : `/groups/${group?.id}/media`, + name: group?.slug ? '/group/:groupSlug' : '/groups/:groupId/media', }, ]; @@ -141,4 +142,4 @@ const GroupPage: React.FC = ({ params, children }) => { ); }; -export default GroupPage; +export default GroupLookupHoc(GroupPage as any) as any; From f2d5b2eaefafcf58eb6ace34c61edc5b6ec0d2a4 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 17 Apr 2023 15:42:08 -0400 Subject: [PATCH 15/24] Update group URLs to use slugs --- .../components/groups/popover/group-popover.tsx | 2 +- app/soapbox/components/status.tsx | 2 +- .../group/components/group-action-button.tsx | 2 +- app/soapbox/features/group/group-blocked-members.tsx | 2 +- app/soapbox/features/group/group-members.tsx | 5 ++++- app/soapbox/features/group/manage-group.tsx | 8 ++++---- .../groups/components/discover/group-grid-item.tsx | 2 +- .../groups/components/discover/group-list-item.tsx | 2 +- app/soapbox/features/groups/index.tsx | 2 +- app/soapbox/features/groups/pending-requests.tsx | 2 +- app/soapbox/features/status/index.tsx | 7 +++++-- app/soapbox/pages/group-page.tsx | 12 ++++++------ 12 files changed, 27 insertions(+), 21 deletions(-) diff --git a/app/soapbox/components/groups/popover/group-popover.tsx b/app/soapbox/components/groups/popover/group-popover.tsx index 752deeb8a..ba2457fb3 100644 --- a/app/soapbox/components/groups/popover/group-popover.tsx +++ b/app/soapbox/components/groups/popover/group-popover.tsx @@ -80,7 +80,7 @@ const GroupPopover = (props: IGroupPopoverContainer) => {

- + 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/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/group-blocked-members.tsx b/app/soapbox/features/group/group-blocked-members.tsx index 94b033837..988b90b1b 100644 --- a/app/soapbox/features/group/group-blocked-members.tsx +++ b/app/soapbox/features/group/group-blocked-members.tsx @@ -86,7 +86,7 @@ const GroupBlockedMembers: React.FC = ({ params }) => { const emptyMessage = ; return ( - + = (props) => { itemClassName='py-3 last:pb-0' prepend={(pendingCount > 0) && (
- +
)} > diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 38b90e34b..dd3efe9cd 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -77,12 +77,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' > - + { 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 818d83720..aaae4b9c7 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -119,6 +119,7 @@ type DisplayMedia = 'default' | 'hide_all' | 'show_all'; type RouteParams = { statusId: string groupId?: string + groupSlug?: string }; interface IThread { @@ -517,8 +518,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/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index 61e41ad25..13dda5467 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -77,19 +77,19 @@ const GroupPage: React.FC = ({ params, children }) => { const items = [ { text: intl.formatMessage(messages.all), - to: group?.slug ? `/group/${group.slug}` : `/groups/${group?.id}`, - name: group?.slug ? '/group/:groupSlug' : '/groups/:groupId', + to: `/group/${group?.slug}`, + name: '/group/:groupSlug', }, { text: intl.formatMessage(messages.members), - to: group?.slug ? `/group/${group.slug}/members` : `/groups/${group?.id}/members`, - name: group?.slug ? '/group/:groupSlug/members' : '/groups/:groupId/members', + to: `/group/${group?.slug}/members`, + name: '/group/:groupSlug/members', count: pending.length, }, { text: intl.formatMessage(messages.media), - to: group?.slug ? `/group/${group.slug}/media` : `/groups/${group?.id}/media`, - name: group?.slug ? '/group/:groupSlug' : '/groups/:groupId/media', + to: `/group/${group?.slug}/media`, + name: '/group/:groupSlug', }, ]; From dea9979b3909623afa1883bd5e18b10977e32a40 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 17 Apr 2023 15:45:16 -0400 Subject: [PATCH 16/24] GroupLookup: improve column loading --- app/soapbox/components/hoc/group-lookup-hoc.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/soapbox/components/hoc/group-lookup-hoc.tsx b/app/soapbox/components/hoc/group-lookup-hoc.tsx index b6a125eed..a7c76eed6 100644 --- a/app/soapbox/components/hoc/group-lookup-hoc.tsx +++ b/app/soapbox/components/hoc/group-lookup-hoc.tsx @@ -3,6 +3,8 @@ import React from 'react'; import ColumnLoading from 'soapbox/features/ui/components/column-loading'; import { useGroupLookup } from 'soapbox/hooks/api/groups/useGroupLookup'; +import { Layout } from '../ui'; + interface IGroupLookup { params: { groupSlug: string @@ -21,7 +23,13 @@ function GroupLookupHoc(Component: React.ComponentType<{ params: { groupId: stri const { entity: group } = useGroupLookup(props.params.groupSlug); if (!group) return ( - + <> + + + + + + ); const newProps = { From 7b544d2427cb62cc123b867d1433478e9911b8f7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 17 Apr 2023 16:34:48 -0400 Subject: [PATCH 17/24] Group Link Preview --- app/soapbox/components/status-media.tsx | 5 +++ .../groups/components/group-link-preview.tsx | 43 +++++++++++++++++++ app/soapbox/normalizers/card.ts | 12 ++++++ 3 files changed, 60 insertions(+) create mode 100644 app/soapbox/features/groups/components/group-link-preview.tsx diff --git a/app/soapbox/components/status-media.tsx b/app/soapbox/components/status-media.tsx index 0867e6ca2..192a4c169 100644 --- a/app/soapbox/components/status-media.tsx +++ b/app/soapbox/components/status-media.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { openModal } from 'soapbox/actions/modals'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; +import { GroupLinkPreview } from 'soapbox/features/groups/components/group-link-preview'; import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder-card'; import Card from 'soapbox/features/status/components/card'; import Bundle from 'soapbox/features/ui/components/bundle'; @@ -153,6 +154,10 @@ const StatusMedia: React.FC = ({ ); } + } else if (status.spoiler_text.length === 0 && !status.quote && status.card?.group) { + media = ( + + ); } else if (status.spoiler_text.length === 0 && !status.quote && status.card) { media = ( = ({ card }) => { + const history = useHistory(); + + const { group } = card; + if (!group) return null; + + const navigateToGroup = () => history.push(`/groups/${group.id}`); + + return ( + +
+ + + + + } /> + + + + + ); +}; + +export { GroupLinkPreview }; \ No newline at end of file diff --git a/app/soapbox/normalizers/card.ts b/app/soapbox/normalizers/card.ts index 536d12c38..5d0af42cf 100644 --- a/app/soapbox/normalizers/card.ts +++ b/app/soapbox/normalizers/card.ts @@ -7,6 +7,7 @@ import punycode from 'punycode'; import { Record as ImmutableRecord, Map as ImmutableMap, fromJS } from 'immutable'; +import { groupSchema, type Group } from 'soapbox/schemas'; import { mergeDefined } from 'soapbox/utils/normalizers'; // https://docs.joinmastodon.org/entities/card/ @@ -16,6 +17,7 @@ export const CardRecord = ImmutableRecord({ blurhash: null as string | null, description: '', embed_url: '', + group: null as null | Group, height: 0, html: '', image: null as string | null, @@ -60,11 +62,21 @@ const normalizeProviderName = (card: ImmutableMap) => { return card.set('provider_name', providerName); }; +const normalizeGroup = (card: ImmutableMap) => { + try { + const group = groupSchema.parse(card.get('group').toJS()); + return card.set('group', group); + } catch (_e) { + return card.set('group', null); + } +}; + export const normalizeCard = (card: Record) => { return CardRecord( ImmutableMap(fromJS(card)).withMutations(card => { normalizePleromaOpengraph(card); normalizeProviderName(card); + normalizeGroup(card); }), ); }; From a461ef9f51b08285d6e3134648eac2b41ac5e2c5 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 17 Apr 2023 16:55:45 -0400 Subject: [PATCH 18/24] Fix endpoint --- app/soapbox/hooks/api/groups/useGroupTags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/hooks/api/groups/useGroupTags.ts b/app/soapbox/hooks/api/groups/useGroupTags.ts index 42d81688f..b2c29aa3c 100644 --- a/app/soapbox/hooks/api/groups/useGroupTags.ts +++ b/app/soapbox/hooks/api/groups/useGroupTags.ts @@ -10,7 +10,7 @@ function useGroupTags(groupId: string) { const { entities, ...result } = useEntities( [Entities.GROUP_TAGS, groupId], - () => api.get(`api/v1/truth/trends/groups/${groupId}/tags`), + () => api.get(`/api/v1/truth/trends/groups/${groupId}/tags`), { schema: groupTagSchema }, ); From 3f44e6cdcb8b7f27d2c5aecd5ad5ae86bde3dc0f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 18 Apr 2023 09:31:08 -0400 Subject: [PATCH 19/24] GroupLinkPreview: fix cursor --- app/soapbox/features/groups/components/group-link-preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/groups/components/group-link-preview.tsx b/app/soapbox/features/groups/components/group-link-preview.tsx index 3cc7f1708..dc5770281 100644 --- a/app/soapbox/features/groups/components/group-link-preview.tsx +++ b/app/soapbox/features/groups/components/group-link-preview.tsx @@ -17,7 +17,7 @@ const GroupLinkPreview: React.FC = ({ card }) => { const navigateToGroup = () => history.push(`/groups/${group.id}`); return ( - +
Date: Tue, 18 Apr 2023 09:39:09 -0400 Subject: [PATCH 20/24] GroupLinkPreview: dark mode --- app/soapbox/features/groups/components/group-link-preview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/groups/components/group-link-preview.tsx b/app/soapbox/features/groups/components/group-link-preview.tsx index dc5770281..be0e9f746 100644 --- a/app/soapbox/features/groups/components/group-link-preview.tsx +++ b/app/soapbox/features/groups/components/group-link-preview.tsx @@ -17,14 +17,14 @@ const GroupLinkPreview: React.FC = ({ card }) => { const navigateToGroup = () => history.push(`/groups/${group.id}`); return ( - +
From 70c2c5c4385cd84ea4b1be00bb484bdcaed9922b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 18 Apr 2023 10:00:18 -0400 Subject: [PATCH 21/24] GroupLinkPreview: link to slug URL --- app/soapbox/features/groups/components/group-link-preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/features/groups/components/group-link-preview.tsx b/app/soapbox/features/groups/components/group-link-preview.tsx index be0e9f746..18ca586a5 100644 --- a/app/soapbox/features/groups/components/group-link-preview.tsx +++ b/app/soapbox/features/groups/components/group-link-preview.tsx @@ -14,7 +14,7 @@ const GroupLinkPreview: React.FC = ({ card }) => { const { group } = card; if (!group) return null; - const navigateToGroup = () => history.push(`/groups/${group.id}`); + const navigateToGroup = () => history.push(`/group/${group.slug}`); return ( From 2216dc4d4cc34a1bd9aaadcb273b4738c293b0f8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 18 Apr 2023 10:40:11 -0400 Subject: [PATCH 22/24] ProfileHoverCard: fix rounded corners --- app/soapbox/components/profile-hover-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/soapbox/components/profile-hover-card.tsx b/app/soapbox/components/profile-hover-card.tsx index 2dd6a3fdd..d0c8a93d4 100644 --- a/app/soapbox/components/profile-hover-card.tsx +++ b/app/soapbox/components/profile-hover-card.tsx @@ -106,7 +106,7 @@ export const ProfileHoverCard: React.FC = ({ visible = true } onMouseEnter={handleMouseEnter(dispatch)} onMouseLeave={handleMouseLeave(dispatch)} > - + From 0f29c5673cb205cc596f038698677de5c251076c Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 18 Apr 2023 12:03:42 -0400 Subject: [PATCH 23/24] Fix bug with slug in Group Media --- app/soapbox/features/group/group-gallery.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx index f8a259e50..f5b1164e7 100644 --- a/app/soapbox/features/group/group-gallery.tsx +++ b/app/soapbox/features/group/group-gallery.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router-dom'; import { openModal } from 'soapbox/actions/modals'; import LoadMore from 'soapbox/components/load-more'; @@ -13,10 +12,14 @@ import MediaItem from '../account-gallery/components/media-item'; import type { Attachment, Status } from 'soapbox/types/entities'; -const GroupGallery = () => { - const dispatch = useAppDispatch(); +interface IGroupGallery { + params: { groupId: string } +} - const { groupId } = useParams<{ groupId: string }>(); +const GroupGallery: React.FC = (props) => { + const { groupId } = props.params; + + const dispatch = useAppDispatch(); const { group, isLoading: groupIsLoading } = useGroup(groupId); From e88be5fadacecf575d6aefa398f1e201a6abfeb8 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Tue, 18 Apr 2023 12:03:52 -0400 Subject: [PATCH 24/24] Fix spacing --- app/soapbox/features/group/group-gallery.tsx | 23 ++++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx index f5b1164e7..96ed47e13 100644 --- a/app/soapbox/features/group/group-gallery.tsx +++ b/app/soapbox/features/group/group-gallery.tsx @@ -27,6 +27,7 @@ const GroupGallery: React.FC = (props) => { entities: statuses, fetchNextPage, isLoading, + isFetching, hasNextPage, } = useGroupMedia(groupId); @@ -48,21 +49,25 @@ const GroupGallery: React.FC = (props) => { if (isLoading || groupIsLoading) { return ( - - + +
+ +
); } if (!group) { return ( - +
+ +
); } return ( -
+
{attachments.map((attachment) => ( = (props) => {
)} - - {(hasNextPage && !isLoading) && ( - - )}
- {isLoading && ( -
- -
+ {hasNextPage && ( + )}
);