From 285ba2bf75b5f0441246734c2c4d4c314eae363f Mon Sep 17 00:00:00 2001 From: teodorus-nathaniel Date: Wed, 27 Dec 2023 03:12:13 +0700 Subject: [PATCH] feat: use data from subid for creators list fix issue where useFetchCreator if fetched multiple times are calling upsert everytime, changing the state, even though the call to api is only once, making it infinite loop --- .../creators/CreatorDashboardSidebar.tsx | 8 +- src/components/main/types.ts | 7 +- src/components/main/utils.ts | 2 +- src/components/spaces/LatestSpacesPage.tsx | 75 +++++++++++++------ src/components/spaces/ViewSpace.tsx | 8 +- .../spaces/helpers/SpaceDropdownMenu.tsx | 4 +- src/components/utils/FollowSpaceButton.tsx | 2 - src/components/utils/OffchainUtils.ts | 4 +- src/config/app/polkaverse/index.ts | 16 ---- src/config/types.ts | 1 - src/rtk/app/rootReducer.ts | 2 + .../features/creators/creatorsListHooks.ts | 20 +++++ .../features/creators/creatorsListSlice.ts | 47 ++++++++++++ src/rtk/features/creators/stakesSlice.ts | 14 ++-- src/rtk/features/creators/totalStakeSlice.ts | 55 +++++++------- 15 files changed, 175 insertions(+), 90 deletions(-) create mode 100644 src/rtk/features/creators/creatorsListHooks.ts create mode 100644 src/rtk/features/creators/creatorsListSlice.ts diff --git a/src/components/creators/CreatorDashboardSidebar.tsx b/src/components/creators/CreatorDashboardSidebar.tsx index 4b1384f02..015abff58 100644 --- a/src/components/creators/CreatorDashboardSidebar.tsx +++ b/src/components/creators/CreatorDashboardSidebar.tsx @@ -1,7 +1,7 @@ import { SpaceData } from '@subsocial/api/types' import clsx from 'clsx' import { ComponentProps } from 'react' -import config from 'src/config' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' import { useMyAddress } from '../auth/MyAccountsContext' import CreatePostCard from './cards/CreatePostCard' @@ -55,8 +55,9 @@ function HomePageSidebar({ variant }: Extract) { const myAddress = useMyAddress() const { data } = useFetchStakeData(myAddress ?? '', space.id) + const isCreator = useIsCreatorSpace(space.id) - if (!config.creatorIds?.includes(space.id)) { + if (!isCreator) { return } @@ -75,8 +76,9 @@ function SpacePageSidebar({ space }: Extract) { const myAddress = useMyAddress() const { data, loading } = useFetchStakeData(myAddress ?? '', space.id) + const isCreator = useIsCreatorSpace(space.id) - if (!config.creatorIds?.includes(space.id)) { + if (!isCreator) { return ( <> diff --git a/src/components/main/types.ts b/src/components/main/types.ts index bc58f1224..509516a48 100644 --- a/src/components/main/types.ts +++ b/src/components/main/types.ts @@ -6,12 +6,7 @@ import { PostKind } from '../../types/graphql-global-types' export type PostFilterType = 'latest' | 'suggested' | 'liked' | 'commented' -export type SpaceFilterType = - | 'latest' - | 'suggested' - | 'sortByPosts' - | 'sortByFollowers' - | 'creators' +export type SpaceFilterType = 'latest' | 'suggested' | 'sortByPosts' | 'sortByFollowers' export type DateFilterType = 'day' | 'week' | 'month' | 'allTime' diff --git a/src/components/main/utils.ts b/src/components/main/utils.ts index 2e1f29d0e..d78c50ebd 100644 --- a/src/components/main/utils.ts +++ b/src/components/main/utils.ts @@ -51,7 +51,7 @@ const getPostsByFilter: GetEntityFilter = { }, } -const getSpacesByFilter: GetEntityFilter> = { +const getSpacesByFilter: GetEntityFilter> = { latest: { day: q.GET_LATEST_SPACE_IDS, week: q.GET_LATEST_SPACE_IDS, diff --git a/src/components/spaces/LatestSpacesPage.tsx b/src/components/spaces/LatestSpacesPage.tsx index 5dad676f2..a2e3050b5 100644 --- a/src/components/spaces/LatestSpacesPage.tsx +++ b/src/components/spaces/LatestSpacesPage.tsx @@ -5,6 +5,9 @@ import { useSubsocialApi } from 'src/components/substrate/SubstrateContext' import config from 'src/config' import { useDfApolloClient } from 'src/graphql/ApolloProvider' import { GetLatestSpaceIds } from 'src/graphql/__generated__/GetLatestSpaceIds' +import { useAppDispatch } from 'src/rtk/app/store' +import { useFetchCreators } from 'src/rtk/features/creators/creatorsListHooks' +import { fetchCreators } from 'src/rtk/features/creators/creatorsListSlice' import { fetchMyPermissionsBySpaceIds } from 'src/rtk/features/permissions/mySpacePermissionsSlice' import { DataSourceTypes, SpaceId } from 'src/types' import { fetchSpaces } from '../../rtk/features/spaces/spacesSlice' @@ -16,39 +19,49 @@ import { isSuggested, loadSpacesByQuery } from '../main/utils' import { getPageOfIds } from '../utils/getIds' import { PublicSpacePreviewById } from './SpacePreview' -const { recommendedSpaceIds, creatorIds } = config +const { recommendedSpaceIds } = config type Props = { + spaceIds?: SpaceId[] initialSpaceIds?: SpaceId[] + customFetcher?: (config: LoadMoreValues) => Promise totalSpaceCount: number filter: SpaceFilterType dateFilter?: DateFilterType } -const shuffledCreatorIds = shuffle(creatorIds ?? []) -const loadMoreSpacesFn = async (loadMoreValues: LoadMoreValues) => { - const { client, size, page, myAddress, subsocial, dispatch, filter } = loadMoreValues +const loadMoreSpacesFn = async ( + loadMoreValues: LoadMoreValues & { + customFetcher?: (config: LoadMoreValues) => Promise + }, +) => { + const { client, size, page, myAddress, subsocial, dispatch, filter, customFetcher } = + loadMoreValues if (filter === undefined) return [] let spaceIds: string[] = [] - if (filter.type !== 'suggested' && filter.type !== 'creators' && client) { - const offset = (page - 1) * size - const data = await loadSpacesByQuery({ - client, - offset, - filter: { type: filter.type, date: filter.date }, - }) - - const { spaces } = data as GetLatestSpaceIds - spaceIds = spaces.map(value => value.id) + if (customFetcher) { + spaceIds = await customFetcher(loadMoreValues) } else { - if (filter.type === 'creators') { - spaceIds = getPageOfIds(shuffledCreatorIds, { page, size }) - } else spaceIds = getPageOfIds(recommendedSpaceIds, { page, size }) + if (filter.type !== 'suggested' && client) { + const offset = (page - 1) * size + const data = await loadSpacesByQuery({ + client, + offset, + filter: { type: filter.type, date: filter.date }, + }) + + const { spaces } = data as GetLatestSpaceIds + spaceIds = spaces.map(value => value.id) + } else { + spaceIds = getPageOfIds(recommendedSpaceIds, { page, size }) + } } + console.log(spaceIds) + await Promise.all([ dispatch(fetchMyPermissionsBySpaceIds({ api: subsocial, ids: spaceIds, myAddress })), dispatch(fetchSpaces({ api: subsocial, ids: spaceIds, dataSource: DataSourceTypes.SQUID })), @@ -58,7 +71,7 @@ const loadMoreSpacesFn = async (loadMoreValues: LoadMoreValues) } const InfiniteListOfSpaces = (props: Props) => { - const { totalSpaceCount, initialSpaceIds, filter, dateFilter } = props + const { totalSpaceCount, initialSpaceIds, filter, dateFilter, customFetcher } = props const client = useDfApolloClient() const dispatch = useDispatch() const { subsocial } = useSubsocialApi() @@ -76,6 +89,7 @@ const InfiniteListOfSpaces = (props: Props) => { type: filter, date: dateFilter, }, + customFetcher, }) } @@ -109,8 +123,27 @@ export const SuggestedSpaces = () => ( ) -export const CreatorsSpaces = () => ( - -) +let shuffledCreators: string[] | null = null +export const CreatorsSpaces = () => { + const { data: creators } = useFetchCreators() + + const dispatch = useAppDispatch() + const loadCreators = async () => { + if (shuffledCreators) return shuffledCreators + + const res = await dispatch(fetchCreators({})) + const spaceIds = (res.payload as { spaceId: string }[]).map(({ spaceId }) => spaceId) + shuffledCreators = shuffle(spaceIds) + return shuffledCreators + } + return ( + + ) +} export default LatestSpacesPage diff --git a/src/components/spaces/ViewSpace.tsx b/src/components/spaces/ViewSpace.tsx index c57769ddd..37ad7d4bc 100644 --- a/src/components/spaces/ViewSpace.tsx +++ b/src/components/spaces/ViewSpace.tsx @@ -8,9 +8,9 @@ import { ButtonLink } from 'src/components/utils/CustomLinks' import { Segment } from 'src/components/utils/Segment' import { LARGE_AVATAR_SIZE } from 'src/config/Size.config' import { useSetChatEntityConfig, useSetChatOpen } from 'src/rtk/app/hooks' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' import { useFetchStakeData } from 'src/rtk/features/creators/stakesHooks' import { SpaceContent, SpaceData, SpaceId, SpaceStruct, SpaceWithSomeDetails } from 'src/types' -import config from '../../config' import { useSelectProfileSpace } from '../../rtk/features/profiles/profilesHooks' import { useSelectSpace } from '../../rtk/features/spaces/spacesHooks' import { useMyAddress } from '../auth/MyAccountsContext' @@ -66,7 +66,8 @@ export const SpaceNameAsLink = React.memo(({ space, ...props }: SpaceNameAsLinkP }) export const StakeButton = ({ spaceStruct }: { spaceStruct: SpaceStruct }) => { - return config.creatorIds?.includes(spaceStruct.id) ? ( + const isCreator = useIsCreatorSpace(spaceStruct.id) + return isCreator ? ( { } }, [spaceData]) + const isCreatorSpace = useIsCreatorSpace(spaceData?.id) + // We do not return 404 page here, because this component could be used to render a space in list. if (!spaceData) return null @@ -295,7 +298,6 @@ export const InnerViewSpace = (props: Props) => { ) } - const isCreatorSpace = config.creatorIds?.includes(spaceData.id) const showCreatorCards = isCreatorSpace && isMobile return ( diff --git a/src/components/spaces/helpers/SpaceDropdownMenu.tsx b/src/components/spaces/helpers/SpaceDropdownMenu.tsx index 081d9b3af..1b73384d2 100644 --- a/src/components/spaces/helpers/SpaceDropdownMenu.tsx +++ b/src/components/spaces/helpers/SpaceDropdownMenu.tsx @@ -5,11 +5,11 @@ import { editSpaceUrl } from 'src/components/urls' import { isHidden, ViewOnIpfs } from 'src/components/utils' import { BasicDropDownMenuProps, DropdownMenu } from 'src/components/utils/DropDownMenu' import { showSuccessMessage } from 'src/components/utils/Message' -import config from 'src/config' import { useHasUserASpacePermission } from 'src/permissions/checkPermission' import { useSendEvent } from 'src/providers/AnalyticContext' import { useSetChatOpen } from 'src/rtk/app/hooks' import { useAppSelector } from 'src/rtk/app/store' +import { useIsCreatorSpace } from 'src/rtk/features/creators/creatorsListHooks' import { SpaceData } from 'src/types' import { useSelectProfile } from '../../../rtk/features/profiles/profilesHooks' import { useIsUsingEmail, useMyAddress } from '../../auth/MyAccountsContext' @@ -43,7 +43,7 @@ export const SpaceDropdownMenu = (props: SpaceDropDownProps) => { const showMakeAsProfileButton = isMySpace && (!profileSpaceId || profileSpaceId !== id) const sendEvent = useSendEvent() - const isCreatorSpace = config.creatorIds?.includes(struct.id) + const isCreatorSpace = useIsCreatorSpace(struct.id) const hasChatSetup = useAppSelector(state => !!state.chat.entity) const setChatOpen = useSetChatOpen() diff --git a/src/components/utils/FollowSpaceButton.tsx b/src/components/utils/FollowSpaceButton.tsx index 296582353..809440a4f 100644 --- a/src/components/utils/FollowSpaceButton.tsx +++ b/src/components/utils/FollowSpaceButton.tsx @@ -62,8 +62,6 @@ export function InnerFollowSpaceButton(props: InnerFollowSpaceButtonProps) { const label = isFollower ? 'Unfollow' : 'Follow' - console.log(otherProps) - return ( { export const getCreatorList = async () => { const res = await axiosRequest(`${subIdApiUrl}/staking/creator/list`) - const creators = (res?.data as { spaceId: string }[]) || [] - return creators.map(({ spaceId }) => spaceId) + const creators = (res?.data as { spaceId: string; status: 'Active' | '' }[]) || [] + return creators.filter(({ status }) => status === 'Active').map(({ spaceId }) => ({ spaceId })) } export const getTotalStake = async ({ address }: { address: string }) => { diff --git a/src/config/app/polkaverse/index.ts b/src/config/app/polkaverse/index.ts index 9a9eee4df..e1d222bbb 100644 --- a/src/config/app/polkaverse/index.ts +++ b/src/config/app/polkaverse/index.ts @@ -23,22 +23,6 @@ const index: AppConfig = { claimedSpaceIds: ['1', '2', '3', '4', '5'], recommendedSpaceIds: polkaverseSpaces, suggestedTlds: ['sub', 'polka'], - creatorIds: [ - '10124', - '11581', - '7366', - '6953', - '1573', - '11157', - '6283', - // '11414' // inactive - '11566', - '4777', - '1238', - '11844', - '4809', - '10132', - ], } export default index diff --git a/src/config/types.ts b/src/config/types.ts index 4d628480f..5718932a1 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -100,7 +100,6 @@ export type AppConfig = { claimedSpaceIds: SpaceId[] recommendedSpaceIds: SpaceId[] suggestedTlds?: string[] - creatorIds?: string[] } export type CommonSubsocialFeatures = { diff --git a/src/rtk/app/rootReducer.ts b/src/rtk/app/rootReducer.ts index a5d432495..a9d1d2ab3 100644 --- a/src/rtk/app/rootReducer.ts +++ b/src/rtk/app/rootReducer.ts @@ -5,6 +5,7 @@ import chainsInfo from '../features/chainsInfo/chainsInfoSlice' import chat from '../features/chat/chatSlice' import enableConfirmation from '../features/confirmationPopup/enableConfirmationSlice' import contents from '../features/contents/contentsSlice' +import creatorsList from '../features/creators/creatorsListSlice' import stakes from '../features/creators/stakesSlice' import totalStake from '../features/creators/totalStakeSlice' import ordersById from '../features/domainPendingOrders/pendingOrdersSlice' @@ -53,6 +54,7 @@ const rootReducer = combineReducers({ chat, stakes, totalStake, + creatorsList, }) export type RootState = ReturnType diff --git a/src/rtk/features/creators/creatorsListHooks.ts b/src/rtk/features/creators/creatorsListHooks.ts new file mode 100644 index 000000000..58615e584 --- /dev/null +++ b/src/rtk/features/creators/creatorsListHooks.ts @@ -0,0 +1,20 @@ +import { useFetchWithoutApi } from 'src/rtk/app/hooksCommon' +import { useAppSelector } from 'src/rtk/app/store' +import { fetchCreators, selectCreators } from './creatorsListSlice' + +export function useFetchCreators(config?: { enabled?: boolean }) { + const { enabled } = config || {} + const data = useAppSelector(state => selectCreators(state)) + + const props = useFetchWithoutApi(fetchCreators, {}, { enabled }) + + return { + ...props, + data, + } +} + +export function useIsCreatorSpace(spaceId?: string) { + const { data } = useFetchCreators({ enabled: !!spaceId }) + return data.map(({ spaceId }) => spaceId).includes(spaceId ?? '') +} diff --git a/src/rtk/features/creators/creatorsListSlice.ts b/src/rtk/features/creators/creatorsListSlice.ts new file mode 100644 index 000000000..af1bf355a --- /dev/null +++ b/src/rtk/features/creators/creatorsListSlice.ts @@ -0,0 +1,47 @@ +import { createAsyncThunk, createEntityAdapter, createSlice } from '@reduxjs/toolkit' +import { getCreatorList } from 'src/components/utils/OffchainUtils' +import { ThunkApiConfig } from 'src/rtk/app/helpers' +import { RootState } from 'src/rtk/app/rootReducer' + +export type Creator = { spaceId: string } +const sliceName = 'creatorsList' + +const adapter = createEntityAdapter({ + selectId: data => data.spaceId, +}) +const selectors = adapter.getSelectors(state => state.creatorsList) + +export const selectCreators = selectors.selectAll + +let pendingPromise: Promise | null = null +export const fetchCreators = createAsyncThunk( + `${sliceName}/fetchMany`, + async ({ reload }, { getState, dispatch }) => { + if (!reload) { + const fetchedData = selectCreators(getState()) + if (fetchedData.length > 0) return fetchedData + } + if (pendingPromise) return pendingPromise + + async function fetchData() { + const data = await getCreatorList() + await dispatch(slice.actions.setCreators(data)) + return data + } + const promise = fetchData() + pendingPromise = promise + await promise + pendingPromise = null + return promise + }, +) + +const slice = createSlice({ + name: sliceName, + initialState: adapter.getInitialState(), + reducers: { + setCreators: adapter.setAll, + }, +}) + +export default slice.reducer diff --git a/src/rtk/features/creators/stakesSlice.ts b/src/rtk/features/creators/stakesSlice.ts index f7315ed46..e5f8f6748 100644 --- a/src/rtk/features/creators/stakesSlice.ts +++ b/src/rtk/features/creators/stakesSlice.ts @@ -37,7 +37,7 @@ export const fetchStakeData = createAsyncThunk< ThunkApiConfig >( `${sliceName}/fetchOne`, - async ({ address, creatorSpaceId, reload }, { getState }): Promise => { + async ({ address, creatorSpaceId, reload }, { getState, dispatch }): Promise => { const id = getStakeId({ address, creatorSpaceId }) if (!reload) { const fetchedData = selectStakeForCreator(getState(), id) @@ -50,7 +50,10 @@ export const fetchStakeData = createAsyncThunk< const data = await getStakeAmount({ address, spaceId: creatorSpaceId }) let stakeAmount = { stakeAmount: '0', hasStaked: false } if (data) stakeAmount = data - return { address, creatorSpaceId, ...stakeAmount } + const finalData = { address, creatorSpaceId, ...stakeAmount } + + await dispatch(slice.actions.setStakeData(finalData)) + return finalData } const promise = fetchData() currentlyFetchingMap.set(id, promise) @@ -63,11 +66,8 @@ export const fetchStakeData = createAsyncThunk< const slice = createSlice({ name: sliceName, initialState: adapter.getInitialState(), - reducers: {}, - extraReducers: builder => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - builder.addCase(fetchStakeData.fulfilled, adapter.upsertOne) + reducers: { + setStakeData: adapter.upsertOne, }, }) diff --git a/src/rtk/features/creators/totalStakeSlice.ts b/src/rtk/features/creators/totalStakeSlice.ts index 667bde1e2..f346fc04f 100644 --- a/src/rtk/features/creators/totalStakeSlice.ts +++ b/src/rtk/features/creators/totalStakeSlice.ts @@ -26,36 +26,39 @@ export const fetchTotalStake = createAsyncThunk< reload?: boolean }, ThunkApiConfig ->(`${sliceName}/fetchOne`, async ({ address, reload }, { getState }): Promise => { - const id = address - if (!reload) { - const fetchedData = selectTotalStake(getState(), id) - if (fetchedData) return fetchedData - } - const alreadyFetchedPromise = currentlyFetchingMap.get(id) - if (alreadyFetchedPromise) return alreadyFetchedPromise - - async function fetchData() { - const data = await getTotalStake({ address }) - let stakeAmount = { amount: '0', hasStaked: false } - if (data) stakeAmount = data - return { address, ...stakeAmount } - } - const promise = fetchData() - currentlyFetchingMap.set(id, promise) - await promise - currentlyFetchingMap.delete(id) - return promise -}) +>( + `${sliceName}/fetchOne`, + async ({ address, reload }, { getState, dispatch }): Promise => { + const id = address + if (!reload) { + const fetchedData = selectTotalStake(getState(), id) + if (fetchedData) return fetchedData + } + const alreadyFetchedPromise = currentlyFetchingMap.get(id) + if (alreadyFetchedPromise) return alreadyFetchedPromise + + async function fetchData() { + const data = await getTotalStake({ address }) + let stakeAmount = { amount: '0', hasStaked: false } + if (data) stakeAmount = data + const finalData = { address, ...stakeAmount } + + await dispatch(slice.actions.setTotalStake(finalData)) + return finalData + } + const promise = fetchData() + currentlyFetchingMap.set(id, promise) + await promise + currentlyFetchingMap.delete(id) + return promise + }, +) const slice = createSlice({ name: sliceName, initialState: adapter.getInitialState(), - reducers: {}, - extraReducers: builder => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - builder.addCase(fetchTotalStake.fulfilled, adapter.upsertOne) + reducers: { + setTotalStake: adapter.upsertOne, }, })