diff --git a/app/soapbox/actions/timelines.ts b/app/soapbox/actions/timelines.ts index 5e4bd26d6..7ae023338 100644 --- a/app/soapbox/actions/timelines.ts +++ b/app/soapbox/actions/timelines.ts @@ -219,6 +219,9 @@ const expandListTimeline = (id: string, { maxId }: Record = {}, don const expandGroupTimeline = (id: string, { maxId }: Record = {}, done = noOp) => expandTimeline(`group:${id}`, `/api/v1/timelines/group/${id}`, { max_id: maxId }, done); +const expandGroupMediaTimeline = (id: string | number, { maxId }: Record = {}) => + expandTimeline(`group:${id}:media`, `/api/v1/timelines/group/${id}`, { max_id: maxId, only_media: true, limit: 40, with_muted: true }); + const expandHashtagTimeline = (hashtag: string, { maxId, tags }: Record = {}, done = noOp) => { return expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId, @@ -309,6 +312,7 @@ export { expandAccountMediaTimeline, expandListTimeline, expandGroupTimeline, + expandGroupMediaTimeline, expandHashtagTimeline, expandTimelineRequest, expandTimelineSuccess, diff --git a/app/soapbox/features/group/group-timeline.tsx b/app/soapbox/features/group/group-timeline.tsx index bf56f869c..44ea68a2f 100644 --- a/app/soapbox/features/group/group-timeline.tsx +++ b/app/soapbox/features/group/group-timeline.tsx @@ -8,7 +8,7 @@ import { connectGroupStream } from 'soapbox/actions/streaming'; import { expandGroupTimeline } from 'soapbox/actions/timelines'; import { Avatar, HStack, Stack } from 'soapbox/components/ui'; import ComposeForm from 'soapbox/features/compose/components/compose-form'; -import { useAppDispatch, useOwnAccount } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useOwnAccount } from 'soapbox/hooks'; import Timeline from '../ui/components/timeline'; @@ -24,6 +24,8 @@ const GroupTimeline: React.FC = (props) => { const groupId = props.params.id; + const relationship = useAppSelector((state) => state.group_relationships.get(groupId)); + const handleLoadMore = (maxId: string) => { dispatch(expandGroupTimeline(groupId, { maxId })); }; @@ -43,7 +45,7 @@ const GroupTimeline: React.FC = (props) => { return ( - {!!account && ( + {!!account && relationship?.member && (
diff --git a/app/soapbox/features/ui/components/group-media-panel.tsx b/app/soapbox/features/ui/components/group-media-panel.tsx new file mode 100644 index 000000000..02dde13b5 --- /dev/null +++ b/app/soapbox/features/ui/components/group-media-panel.tsx @@ -0,0 +1,89 @@ +import { List as ImmutableList } from 'immutable'; +import React, { useState, useEffect } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { useDispatch } from 'react-redux'; + +import { openModal } from 'soapbox/actions/modals'; +import { expandGroupMediaTimeline } from 'soapbox/actions/timelines'; +import { Spinner, Text, Widget } from 'soapbox/components/ui'; +import { useAppSelector } from 'soapbox/hooks'; +import { getGroupGallery } from 'soapbox/selectors'; + +import MediaItem from '../../account-gallery/components/media-item'; + +import type { Attachment, Group } from 'soapbox/types/entities'; + +interface IGroupMediaPanel { + group?: Group, +} + +const GroupMediaPanel: React.FC = ({ group }) => { + const dispatch = useDispatch(); + + const [loading, setLoading] = useState(true); + + const attachments: ImmutableList = useAppSelector((state) => group ? getGroupGallery(state, group?.id) : ImmutableList()); + + const handleOpenMedia = (attachment: Attachment): void => { + if (attachment.type === 'video') { + dispatch(openModal('VIDEO', { media: attachment, status: attachment.status })); + } else { + const media = attachment.getIn(['status', 'media_attachments']) as ImmutableList; + const index = media.findIndex(x => x.id === attachment.id); + + dispatch(openModal('MEDIA', { media, index, status: attachment.status, account: attachment.account })); + } + }; + + useEffect(() => { + setLoading(true); + + if (group) { + dispatch(expandGroupMediaTimeline(group.id)) + // @ts-ignore + .then(() => setLoading(false)) + .catch(() => {}); + } + }, [group?.id]); + + const renderAttachments = () => { + const nineAttachments = attachments.slice(0, 9); + + if (!nineAttachments.isEmpty()) { + return ( +
+ {nineAttachments.map((attachment, _index) => ( + + ))} +
+ ); + } else { + return ( + + + + ); + } + }; + + return ( + }> + {group && ( +
+ {loading ? ( + + ) : ( + renderAttachments() + )} +
+ )} +
+ ); +}; + +export default GroupMediaPanel; diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index 28705750b..dfa0dd0ff 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -561,3 +561,7 @@ export function ManageGroupModal() { export function NewGroupPanel() { return import(/* webpackChunkName: "features/groups" */'../components/panels/new-group-panel'); } + +export function GroupMediaPanel() { + return import(/* webpackChunkName: "features/groups" */'../components/group-media-panel'); +} diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index 620147872..e47b8b1fd 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -9,8 +9,9 @@ import GroupHeader from 'soapbox/features/group/components/group-header'; import LinkFooter from 'soapbox/features/ui/components/link-footer'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; import { - SignUpPanel, CtaBanner, + GroupMediaPanel, + SignUpPanel, } from 'soapbox/features/ui/util/async-components'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { makeGetGroup } from 'soapbox/selectors'; @@ -90,6 +91,9 @@ const GroupPage: React.FC = ({ params, children }) => { {Component => } )} + + {Component => } + diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 4dc7bf03e..353180cca 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -218,6 +218,25 @@ export const getAccountGallery = createSelector([ }, ImmutableList()); }); +export const getGroupGallery = createSelector([ + (state: RootState, id: string) => state.timelines.get(`group:${id}:media`)?.items || ImmutableOrderedSet(), + (state: RootState) => state.statuses, + (state: RootState) => state.accounts, +], (statusIds, statuses, accounts) => { + + return statusIds.reduce((medias: ImmutableList, statusId: string) => { + const status = statuses.get(statusId); + if (!status) return medias; + if (status.reblog) return medias; + if (typeof status.account !== 'string') return medias; + + const account = accounts.get(status.account); + + return medias.concat( + status.media_attachments.map(media => media.merge({ status, account }))); + }, ImmutableList()); +}); + type APIChat = { id: string, last_message: string }; export const makeGetChat = () => {