Merge branch 'group-gallery' into 'develop'

Groups: add group gallery

See merge request soapbox-pub/soapbox!2427
environments/review-develop-3zknud/deployments/3140
Alex Gleason 1 year ago
commit 8ea099aca8

@ -3,5 +3,6 @@ export enum Entities {
GROUPS = 'Groups',
GROUP_RELATIONSHIPS = 'GroupRelationships',
GROUP_MEMBERSHIPS = 'GroupMemberships',
RELATIONSHIPS = 'Relationships'
RELATIONSHIPS = 'Relationships',
STATUSES = 'Statuses',
}

@ -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<Attachment[]>((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 (
<Column>
<Spinner />
</Column>
);
}
if (!group) {
return (
<MissingIndicator />
);
}
return (
<Column label={group.display_name} transparent withHeader={false}>
<div role='feed' className='grid grid-cols-2 gap-2 sm:grid-cols-3'>
{attachments.map((attachment) => (
<MediaItem
key={`${attachment.status.id}+${attachment.id}`}
attachment={attachment}
onOpenMedia={handleOpenMedia}
/>
))}
{(!isLoading && attachments.length === 0) && (
<div className='empty-column-indicator'>
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
</div>
)}
{(hasNextPage && !isLoading) && (
<LoadMore className='my-auto' visible={!isLoading} onClick={fetchNextPage} />
)}
</div>
{isLoading && (
<div className='slist__append'>
<Spinner />
</div>
)}
</Column>
);
};
export default GroupGallery;

@ -117,6 +117,7 @@ import {
EventInformation,
EventDiscussion,
Events,
GroupGallery,
Groups,
GroupsDiscover,
GroupsPopular,
@ -298,6 +299,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
{features.groupsPending && <WrappedRoute path='/groups/pending-requests' exact page={GroupsPendingPage} component={PendingGroupRequests} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/members' exact page={GroupPage} component={GroupMembers} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/media' publicRoute={!authenticatedProfile} component={GroupGallery} page={GroupPage} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage' exact page={ManageGroupsPage} component={ManageGroup} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage/edit' exact page={ManageGroupsPage} component={EditGroup} content={children} />}
{features.groups && <WrappedRoute path='/groups/:id/manage/blocks' exact page={ManageGroupsPage} component={GroupBlockedMembers} content={children} />}

@ -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');
}

@ -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 };

@ -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';

@ -807,6 +807,7 @@
"group.role.admin": "Admin",
"group.role.owner": "Owner",
"group.tabs.all": "All",
"group.tabs.media": "Media",
"group.tabs.members": "Members",
"group.tags.hint": "Add up to 3 keywords that will serve as core topics of discussion in the group.",
"group.tags.label": "Tags",

@ -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<IGroupPage> = ({ 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 = () => {

@ -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<typeof statusSchema>;
export { statusSchema, type Status };

@ -1,3 +1,5 @@
import z from 'zod';
/** Use new value only if old value is undefined */
export const mergeDefined = (oldVal: any, newVal: any) => oldVal === undefined ? newVal : oldVal;
@ -10,3 +12,18 @@ export const makeEmojiMap = (emojis: any) => emojis.reduce((obj: any, emoji: any
export const normalizeId = (id: any): string | null => {
return typeof id === 'string' ? id : null;
};
export type Normalizer<V, R> = (value: V) => R;
/**
* Allows using any legacy normalizer function as a zod schema.
*
* @example
* ```ts
* const statusSchema = toSchema(normalizeStatus);
* statusSchema.parse(status);
* ```
*/
export const toSchema = <V, R>(normalizer: Normalizer<V, R>) => {
return z.custom<V>().transform<R>(normalizer);
};
Loading…
Cancel
Save