From 901ff57e133e5ff446765754ae137a831f475205 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 10 Apr 2023 12:02:50 -0500 Subject: [PATCH 1/3] Add `toSchema` function, to convert any legacy normalizer into a zod schema --- app/soapbox/utils/normalizers.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/soapbox/utils/normalizers.ts b/app/soapbox/utils/normalizers.ts index 6604f1caa..64638dd29 100644 --- a/app/soapbox/utils/normalizers.ts +++ b/app/soapbox/utils/normalizers.ts @@ -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 = (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 = (normalizer: Normalizer) => { + return z.custom().transform(normalizer); +}; \ No newline at end of file From ce0557546a53792bd7ae6d16f3b64dff440df48a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 10 Apr 2023 15:22:08 -0500 Subject: [PATCH 2/3] Groups: add group gallery --- app/soapbox/entity-store/entities.ts | 3 +- app/soapbox/features/group/group-gallery.tsx | 91 +++++++++++++++++++ app/soapbox/features/ui/index.tsx | 2 + .../features/ui/util/async-components.ts | 4 + app/soapbox/hooks/api/groups/useGroupMedia.ts | 14 +++ app/soapbox/hooks/api/index.ts | 1 + app/soapbox/pages/group-page.tsx | 6 ++ app/soapbox/schemas/status.ts | 10 ++ 8 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/features/group/group-gallery.tsx create mode 100644 app/soapbox/hooks/api/groups/useGroupMedia.ts create mode 100644 app/soapbox/schemas/status.ts 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 From b2b1f5ece37ced16ca320f98647d1cd92dffd8a6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 11 Apr 2023 09:01:59 -0500 Subject: [PATCH 3/3] yarn i18n --- app/soapbox/locales/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 080a6dfc5..265cb1bcc 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -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",