From bcac58b9c386733c2dd82a18d40079f832d6a4d8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 29 Jun 2023 15:10:45 -0500 Subject: [PATCH 01/27] Redirect to login when groups or accounts 403 --- app/soapbox/api/hooks/accounts/useAccount.ts | 13 ++++++++++++- .../api/hooks/accounts/useAccountLookup.ts | 13 ++++++++++++- app/soapbox/api/hooks/groups/useGroup.ts | 13 ++++++++++++- app/soapbox/api/hooks/groups/useGroupLookup.ts | 17 +++++++++++++++-- app/soapbox/entity-store/hooks/useEntity.ts | 12 ++++++++++-- .../entity-store/hooks/useEntityLookup.ts | 8 ++++++-- app/soapbox/features/ui/index.tsx | 2 +- 7 files changed, 68 insertions(+), 10 deletions(-) diff --git a/app/soapbox/api/hooks/accounts/useAccount.ts b/app/soapbox/api/hooks/accounts/useAccount.ts index ac756f6ca..468049441 100644 --- a/app/soapbox/api/hooks/accounts/useAccount.ts +++ b/app/soapbox/api/hooks/accounts/useAccount.ts @@ -1,3 +1,6 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + import { Entities } from 'soapbox/entity-store/entities'; import { useEntity } from 'soapbox/entity-store/hooks'; import { useFeatures, useLoggedIn } from 'soapbox/hooks'; @@ -12,11 +15,12 @@ interface UseAccountOpts { function useAccount(accountId?: string, opts: UseAccountOpts = {}) { const api = useApi(); + const history = useHistory(); const features = useFeatures(); const { me } = useLoggedIn(); const { withRelationship } = opts; - const { entity: account, ...result } = useEntity( + const { entity: account, isUnauthorized, ...result } = useEntity( [Entities.ACCOUNTS, accountId!], () => api.get(`/api/v1/accounts/${accountId}`), { schema: accountSchema, enabled: !!accountId }, @@ -30,10 +34,17 @@ function useAccount(accountId?: string, opts: UseAccountOpts = {}) { const isBlocked = account?.relationship?.blocked_by === true; const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + return { ...result, isLoading: result.isLoading, isRelationshipLoading, + isUnauthorized, isUnavailable, account: account ? { ...account, relationship } : undefined, }; diff --git a/app/soapbox/api/hooks/accounts/useAccountLookup.ts b/app/soapbox/api/hooks/accounts/useAccountLookup.ts index dc7f2fb29..ffe5885de 100644 --- a/app/soapbox/api/hooks/accounts/useAccountLookup.ts +++ b/app/soapbox/api/hooks/accounts/useAccountLookup.ts @@ -1,3 +1,6 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + import { Entities } from 'soapbox/entity-store/entities'; import { useEntityLookup } from 'soapbox/entity-store/hooks'; import { useFeatures, useLoggedIn } from 'soapbox/hooks'; @@ -13,10 +16,11 @@ interface UseAccountLookupOpts { function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = {}) { const api = useApi(); const features = useFeatures(); + const history = useHistory(); const { me } = useLoggedIn(); const { withRelationship } = opts; - const { entity: account, ...result } = useEntityLookup( + const { entity: account, isUnauthorized, ...result } = useEntityLookup( Entities.ACCOUNTS, (account) => account.acct === acct, () => api.get(`/api/v1/accounts/lookup?acct=${acct}`), @@ -31,10 +35,17 @@ function useAccountLookup(acct: string | undefined, opts: UseAccountLookupOpts = const isBlocked = account?.relationship?.blocked_by === true; const isUnavailable = (me === account?.id) ? false : (isBlocked && !features.blockersVisible); + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + return { ...result, isLoading: result.isLoading, isRelationshipLoading, + isUnauthorized, isUnavailable, account: account ? { ...account, relationship } : undefined, }; diff --git a/app/soapbox/api/hooks/groups/useGroup.ts b/app/soapbox/api/hooks/groups/useGroup.ts index 5eb6147d2..9efafb13c 100644 --- a/app/soapbox/api/hooks/groups/useGroup.ts +++ b/app/soapbox/api/hooks/groups/useGroup.ts @@ -1,3 +1,6 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + import { Entities } from 'soapbox/entity-store/entities'; import { useEntity } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks'; @@ -7,8 +10,9 @@ import { useGroupRelationship } from './useGroupRelationship'; function useGroup(groupId: string, refetch = true) { const api = useApi(); + const history = useHistory(); - const { entity: group, ...result } = useEntity( + const { entity: group, isUnauthorized, ...result } = useEntity( [Entities.GROUPS, groupId], () => api.get(`/api/v1/groups/${groupId}`), { @@ -19,8 +23,15 @@ function useGroup(groupId: string, refetch = true) { ); const { groupRelationship: relationship } = useGroupRelationship(groupId); + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + return { ...result, + isUnauthorized, group: group ? { ...group, relationship: relationship || null } : undefined, }; } diff --git a/app/soapbox/api/hooks/groups/useGroupLookup.ts b/app/soapbox/api/hooks/groups/useGroupLookup.ts index 3e66f72c6..e3a979777 100644 --- a/app/soapbox/api/hooks/groups/useGroupLookup.ts +++ b/app/soapbox/api/hooks/groups/useGroupLookup.ts @@ -1,24 +1,37 @@ +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; + import { Entities } from 'soapbox/entity-store/entities'; import { useEntityLookup } from 'soapbox/entity-store/hooks'; import { useApi } from 'soapbox/hooks/useApi'; +import { useFeatures } from 'soapbox/hooks/useFeatures'; import { groupSchema } from 'soapbox/schemas'; import { useGroupRelationship } from './useGroupRelationship'; function useGroupLookup(slug: string) { const api = useApi(); + const features = useFeatures(); + const history = useHistory(); - const { entity: group, ...result } = useEntityLookup( + const { entity: group, isUnauthorized, ...result } = useEntityLookup( Entities.GROUPS, (group) => group.slug === slug, () => api.get(`/api/v1/groups/lookup?name=${slug}`), - { schema: groupSchema, enabled: !!slug }, + { schema: groupSchema, enabled: features.groups && !!slug }, ); const { groupRelationship: relationship } = useGroupRelationship(group?.id); + useEffect(() => { + if (isUnauthorized) { + history.push('/login'); + } + }, [isUnauthorized]); + return { ...result, + isUnauthorized, entity: group ? { ...group, relationship: relationship || null } : undefined, }; } diff --git a/app/soapbox/entity-store/hooks/useEntity.ts b/app/soapbox/entity-store/hooks/useEntity.ts index 3d57c8ab0..b9b9f001f 100644 --- a/app/soapbox/entity-store/hooks/useEntity.ts +++ b/app/soapbox/entity-store/hooks/useEntity.ts @@ -1,4 +1,5 @@ -import { useEffect } from 'react'; +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; import z from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; @@ -24,6 +25,8 @@ function useEntity( opts: UseEntityOpts = {}, ) { const [isFetching, setPromise] = useLoading(true); + const [error, setError] = useState(); + const dispatch = useAppDispatch(); const [entityType, entityId] = path; @@ -35,6 +38,7 @@ function useEntity( const isEnabled = opts.enabled ?? true; const isLoading = isFetching && !entity; + const isLoaded = !isFetching && !!entity; const fetchEntity = async () => { try { @@ -42,7 +46,7 @@ function useEntity( const entity = schema.parse(response.data); dispatch(importEntities([entity], entityType)); } catch (e) { - // do nothing + setError(e); } }; @@ -58,6 +62,10 @@ function useEntity( fetchEntity, isFetching, isLoading, + isLoaded, + error, + isUnauthorized: error instanceof AxiosError && error.response?.status === 401, + isForbidden: error instanceof AxiosError && error.response?.status === 403, }; } diff --git a/app/soapbox/entity-store/hooks/useEntityLookup.ts b/app/soapbox/entity-store/hooks/useEntityLookup.ts index 73b2ef938..29cf85244 100644 --- a/app/soapbox/entity-store/hooks/useEntityLookup.ts +++ b/app/soapbox/entity-store/hooks/useEntityLookup.ts @@ -1,4 +1,5 @@ -import { useEffect } from 'react'; +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; import { z } from 'zod'; import { useAppDispatch, useAppSelector, useLoading } from 'soapbox/hooks'; @@ -23,6 +24,7 @@ function useEntityLookup( const dispatch = useAppDispatch(); const [isFetching, setPromise] = useLoading(true); + const [error, setError] = useState(); const entity = useAppSelector(state => findEntity(state, entityType, lookupFn)); const isEnabled = opts.enabled ?? true; @@ -34,7 +36,7 @@ function useEntityLookup( const entity = schema.parse(response.data); dispatch(importEntities([entity], entityType)); } catch (e) { - // do nothing + setError(e); } }; @@ -51,6 +53,8 @@ function useEntityLookup( fetchEntity, isFetching, isLoading, + isUnauthorized: error instanceof AxiosError && error.response?.status === 401, + isForbidden: error instanceof AxiosError && error.response?.status === 403, }; } diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 847b29d26..6b81e38d5 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -327,7 +327,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groupsTags && } {features.groupsTags && } - {features.groups && } + {features.groups && } {features.groups && } {features.groups && } {features.groups && } From a9db41de89cdfcfabf469c938c903790b17d5934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 5 Jul 2023 21:39:50 +0200 Subject: [PATCH 02/27] Use AvatarPicker/HeaderPicker on Edit Profile page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../api/hooks/groups/useUpdateGroup.ts | 6 +- app/soapbox/components/still-image.tsx | 2 +- .../components/avatar-picker.tsx} | 18 ++-- .../components/header-picker.tsx} | 26 ++++-- .../components/profile-preview.tsx | 49 ----------- app/soapbox/features/edit-profile/index.tsx | 85 ++++++------------- app/soapbox/features/group/edit-group.tsx | 9 +- .../manage-group-modal/steps/details-step.tsx | 8 +- app/soapbox/hooks/forms/useImageField.ts | 10 ++- app/soapbox/hooks/forms/usePreview.ts | 2 +- 10 files changed, 79 insertions(+), 136 deletions(-) rename app/soapbox/features/{group/components/group-avatar-picker.tsx => edit-profile/components/avatar-picker.tsx} (61%) rename app/soapbox/features/{group/components/group-header-picker.tsx => edit-profile/components/header-picker.tsx} (64%) delete mode 100644 app/soapbox/features/edit-profile/components/profile-preview.tsx diff --git a/app/soapbox/api/hooks/groups/useUpdateGroup.ts b/app/soapbox/api/hooks/groups/useUpdateGroup.ts index bb3a37dc2..eb69670b1 100644 --- a/app/soapbox/api/hooks/groups/useUpdateGroup.ts +++ b/app/soapbox/api/hooks/groups/useUpdateGroup.ts @@ -6,8 +6,8 @@ import { groupSchema } from 'soapbox/schemas'; interface UpdateGroupParams { display_name?: string note?: string - avatar?: File - header?: File + avatar?: File | '' | null + header?: File | '' | null group_visibility?: string discoverable?: boolean tags?: string[] @@ -30,4 +30,4 @@ function useUpdateGroup(groupId: string) { }; } -export { useUpdateGroup }; \ No newline at end of file +export { useUpdateGroup }; diff --git a/app/soapbox/components/still-image.tsx b/app/soapbox/components/still-image.tsx index cdebaf359..4a3caf01e 100644 --- a/app/soapbox/components/still-image.tsx +++ b/app/soapbox/components/still-image.tsx @@ -67,7 +67,7 @@ const StillImage: React.FC = ({ alt, className, src, style, letterb )} diff --git a/app/soapbox/features/group/components/group-avatar-picker.tsx b/app/soapbox/features/edit-profile/components/avatar-picker.tsx similarity index 61% rename from app/soapbox/features/group/components/group-avatar-picker.tsx rename to app/soapbox/features/edit-profile/components/avatar-picker.tsx index b13dfe80e..09a6b0d29 100644 --- a/app/soapbox/features/group/components/group-avatar-picker.tsx +++ b/app/soapbox/features/edit-profile/components/avatar-picker.tsx @@ -1,19 +1,25 @@ import clsx from 'clsx'; import React from 'react'; +import { FormattedMessage } from 'react-intl'; -import Icon from 'soapbox/components/icon'; -import { Avatar, HStack } from 'soapbox/components/ui'; +import { Avatar, Icon, HStack } from 'soapbox/components/ui'; interface IMediaInput { + className?: string src: string | undefined accept: string onChange: React.ChangeEventHandler disabled?: boolean } -const AvatarPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { +const AvatarPicker = React.forwardRef(({ className, src, onChange, accept, disabled }, ref) => { return ( -