diff --git a/app/soapbox/entity-store/entities.ts b/app/soapbox/entity-store/entities.ts index 7f9f84e2a..719cc7f1c 100644 --- a/app/soapbox/entity-store/entities.ts +++ b/app/soapbox/entity-store/entities.ts @@ -3,5 +3,6 @@ export enum Entities { GROUPS = 'Groups', GROUP_RELATIONSHIPS = 'GroupRelationships', GROUP_MEMBERSHIPS = 'GroupMemberships', - RELATIONSHIPS = 'Relationships' + RELATIONSHIPS = 'Relationships', + STATUSES = 'Statuses', } \ No newline at end of file diff --git a/app/soapbox/features/group/group-gallery.tsx b/app/soapbox/features/group/group-gallery.tsx new file mode 100644 index 000000000..139be5bf3 --- /dev/null +++ b/app/soapbox/features/group/group-gallery.tsx @@ -0,0 +1,91 @@ +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'; +import MissingIndicator from 'soapbox/components/missing-indicator'; +import { Column, Spinner } from 'soapbox/components/ui'; +import { useAppDispatch } from 'soapbox/hooks'; +import { useGroup, useGroupMedia } from 'soapbox/hooks/api'; + +import MediaItem from '../account-gallery/components/media-item'; + +import type { Attachment, Status } from 'soapbox/types/entities'; + +const GroupGallery = () => { + const dispatch = useAppDispatch(); + + const { id: groupId } = useParams<{ id: string }>(); + + const { group, isLoading: groupIsLoading } = useGroup(groupId); + + const { + entities: statuses, + fetchNextPage, + isLoading, + hasNextPage, + } = useGroupMedia(groupId); + + const attachments = statuses.reduce((result, status) => { + result.push(...status.media_attachments.map((a) => a.set('status', status))); + return result; + }, []); + + const handleOpenMedia = (attachment: Attachment) => { + if (attachment.type === 'video') { + dispatch(openModal('VIDEO', { media: attachment, status: attachment.status, account: attachment.account })); + } else { + const media = (attachment.status as Status).media_attachments; + const index = media.findIndex((x) => x.id === attachment.id); + + dispatch(openModal('MEDIA', { media, index, status: attachment.status })); + } + }; + + if (isLoading || groupIsLoading) { + return ( + + + + ); + } + + if (!group) { + return ( + + ); + } + + return ( + +
+ {attachments.map((attachment) => ( + + ))} + + {(!isLoading && attachments.length === 0) && ( +
+ +
+ )} + + {(hasNextPage && !isLoading) && ( + + )} +
+ + {isLoading && ( +
+ +
+ )} +
+ ); +}; + +export default GroupGallery; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 4fd8d9394..f5e8bbd85 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -116,6 +116,7 @@ import { EventInformation, EventDiscussion, Events, + GroupGallery, Groups, GroupsDiscover, GroupsPopular, @@ -297,6 +298,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groupsPending && } {features.groups && } {features.groups && } + {features.groups && } {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 5296504da..f915571d2 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -590,6 +590,10 @@ export function GroupMembershipRequests() { return import(/* webpackChunkName: "features/groups" */'../../group/group-membership-requests'); } +export function GroupGallery() { + return import(/* webpackChunkName: "features/groups" */'../../group/group-gallery'); +} + export function CreateGroupModal() { return import(/* webpackChunkName: "features/groups" */'../components/modals/manage-group-modal/create-group-modal'); } diff --git a/app/soapbox/hooks/api/groups/useGroupMedia.ts b/app/soapbox/hooks/api/groups/useGroupMedia.ts new file mode 100644 index 000000000..23375bdc7 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupMedia.ts @@ -0,0 +1,14 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi } from 'soapbox/hooks/useApi'; +import { statusSchema } from 'soapbox/schemas/status'; + +function useGroupMedia(groupId: string) { + const api = useApi(); + + return useEntities([Entities.STATUSES, 'groupMedia', groupId], () => { + return api.get(`/api/v1/timelines/group/${groupId}?only_media=true`); + }, { schema: statusSchema }); +} + +export { useGroupMedia }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index 4006c1ba9..b5927bf6b 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -11,6 +11,7 @@ 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 { useGroupMembershipRequests } from './groups/useGroupMembershipRequests'; export { useGroupSearch } from './groups/useGroupSearch'; diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index ee6f46cb6..133679c4a 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -22,6 +22,7 @@ import { Tabs } from '../components/ui'; const messages = defineMessages({ all: { id: 'group.tabs.all', defaultMessage: 'All' }, members: { id: 'group.tabs.members', defaultMessage: 'Members' }, + media: { id: 'group.tabs.media', defaultMessage: 'Media' }, }); interface IGroupPage { @@ -84,6 +85,11 @@ const GroupPage: React.FC = ({ params, children }) => { name: '/groups/:id/members', count: pending.length, }, + { + text: intl.formatMessage(messages.media), + to: `/groups/${group?.id}/media`, + name: '/groups/:id/media', + }, ]; const renderChildren = () => { diff --git a/app/soapbox/schemas/status.ts b/app/soapbox/schemas/status.ts new file mode 100644 index 000000000..66d6f05eb --- /dev/null +++ b/app/soapbox/schemas/status.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +import { normalizeStatus } from 'soapbox/normalizers'; +import { toSchema } from 'soapbox/utils/normalizers'; + +const statusSchema = toSchema(normalizeStatus); + +type Status = z.infer; + +export { statusSchema, type Status }; \ No newline at end of file