kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Add React Query
rodzic
a8577fe0ee
commit
7836698dc2
|
@ -1,58 +0,0 @@
|
||||||
import { __stub } from 'soapbox/api';
|
|
||||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
|
||||||
|
|
||||||
import { fetchCarouselAvatars } from '../carousels';
|
|
||||||
|
|
||||||
describe('fetchCarouselAvatars()', () => {
|
|
||||||
let store: ReturnType<typeof mockStore>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = mockStore(rootState);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with a successful API request', () => {
|
|
||||||
let avatars: Record<string, any>[];
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
avatars = [
|
|
||||||
{ account_id: '1', acct: 'jl', account_avatar: 'https://example.com/some.jpg' },
|
|
||||||
];
|
|
||||||
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/truth/carousels/avatars').reply(200, avatars);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch the users from the API', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'CAROUSEL_AVATAR_REQUEST' },
|
|
||||||
{ type: 'CAROUSEL_AVATAR_SUCCESS', payload: avatars },
|
|
||||||
];
|
|
||||||
|
|
||||||
await store.dispatch(fetchCarouselAvatars());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('with an unsuccessful API request', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
__stub((mock) => {
|
|
||||||
mock.onGet('/api/v1/truth/carousels/avatars').networkError();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should dispatch failed action', async() => {
|
|
||||||
const expectedActions = [
|
|
||||||
{ type: 'CAROUSEL_AVATAR_REQUEST' },
|
|
||||||
{ type: 'CAROUSEL_AVATAR_FAIL' },
|
|
||||||
];
|
|
||||||
|
|
||||||
await store.dispatch(fetchCarouselAvatars());
|
|
||||||
const actions = store.getActions();
|
|
||||||
|
|
||||||
expect(actions).toEqual(expectedActions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -202,9 +202,7 @@ export const rememberAuthAccount = (accountUrl: string) =>
|
||||||
|
|
||||||
export const loadCredentials = (token: string, accountUrl: string) =>
|
export const loadCredentials = (token: string, accountUrl: string) =>
|
||||||
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
(dispatch: AppDispatch) => dispatch(rememberAuthAccount(accountUrl))
|
||||||
.then(() => {
|
.then(() => dispatch(verifyCredentials(token, accountUrl)))
|
||||||
dispatch(verifyCredentials(token, accountUrl));
|
|
||||||
})
|
|
||||||
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
.catch(() => dispatch(verifyCredentials(token, accountUrl)));
|
||||||
|
|
||||||
/** Trim the username and strip the leading @. */
|
/** Trim the username and strip the leading @. */
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
import { AxiosResponse } from 'axios';
|
|
||||||
|
|
||||||
import { AppDispatch, RootState } from 'soapbox/store';
|
|
||||||
|
|
||||||
import api from '../api';
|
|
||||||
|
|
||||||
const CAROUSEL_AVATAR_REQUEST = 'CAROUSEL_AVATAR_REQUEST';
|
|
||||||
const CAROUSEL_AVATAR_SUCCESS = 'CAROUSEL_AVATAR_SUCCESS';
|
|
||||||
const CAROUSEL_AVATAR_FAIL = 'CAROUSEL_AVATAR_FAIL';
|
|
||||||
|
|
||||||
const fetchCarouselAvatars = () => (dispatch: AppDispatch, getState: () => RootState) => {
|
|
||||||
dispatch({ type: CAROUSEL_AVATAR_REQUEST });
|
|
||||||
|
|
||||||
return api(getState)
|
|
||||||
.get('/api/v1/truth/carousels/avatars')
|
|
||||||
.then((response: AxiosResponse) => dispatch({ type: CAROUSEL_AVATAR_SUCCESS, payload: response.data }))
|
|
||||||
.catch(() => dispatch({ type: CAROUSEL_AVATAR_FAIL }));
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
CAROUSEL_AVATAR_REQUEST,
|
|
||||||
CAROUSEL_AVATAR_SUCCESS,
|
|
||||||
CAROUSEL_AVATAR_FAIL,
|
|
||||||
fetchCarouselAvatars,
|
|
||||||
};
|
|
|
@ -17,6 +17,7 @@ import * as BuildConfig from 'soapbox/build_config';
|
||||||
import GdprBanner from 'soapbox/components/gdpr-banner';
|
import GdprBanner from 'soapbox/components/gdpr-banner';
|
||||||
import Helmet from 'soapbox/components/helmet';
|
import Helmet from 'soapbox/components/helmet';
|
||||||
import LoadingScreen from 'soapbox/components/loading-screen';
|
import LoadingScreen from 'soapbox/components/loading-screen';
|
||||||
|
import { AuthProvider, useAuth } from 'soapbox/contexts/auth-context';
|
||||||
import AuthLayout from 'soapbox/features/auth_layout';
|
import AuthLayout from 'soapbox/features/auth_layout';
|
||||||
import PublicLayout from 'soapbox/features/public_layout';
|
import PublicLayout from 'soapbox/features/public_layout';
|
||||||
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
|
||||||
|
@ -38,7 +39,7 @@ import {
|
||||||
useLocale,
|
useLocale,
|
||||||
} from 'soapbox/hooks';
|
} from 'soapbox/hooks';
|
||||||
import MESSAGES from 'soapbox/locales/messages';
|
import MESSAGES from 'soapbox/locales/messages';
|
||||||
import { queryClient } from 'soapbox/queries/client';
|
import { queryClient, useAxiosInterceptors } from 'soapbox/queries/client';
|
||||||
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
import { useCachedLocationHandler } from 'soapbox/utils/redirect';
|
||||||
import { generateThemeCss } from 'soapbox/utils/theme';
|
import { generateThemeCss } from 'soapbox/utils/theme';
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ const loadInitial = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
return async(dispatch, getState) => {
|
return async(dispatch, getState) => {
|
||||||
// Await for authenticated fetch
|
// Await for authenticated fetch
|
||||||
await dispatch(fetchMe());
|
const account = await dispatch(fetchMe());
|
||||||
// Await for feature detection
|
// Await for feature detection
|
||||||
await dispatch(loadInstance());
|
await dispatch(loadInstance());
|
||||||
// Await for configuration
|
// Await for configuration
|
||||||
|
@ -75,12 +76,15 @@ const loadInitial = () => {
|
||||||
if (pepeEnabled && !state.me) {
|
if (pepeEnabled && !state.me) {
|
||||||
await dispatch(fetchVerificationConfig());
|
await dispatch(fetchVerificationConfig());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Highest level node with the Redux store. */
|
/** Highest level node with the Redux store. */
|
||||||
const SoapboxMount = () => {
|
const SoapboxMount = () => {
|
||||||
useCachedLocationHandler();
|
useCachedLocationHandler();
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const instance = useAppSelector(state => state.instance);
|
const instance = useAppSelector(state => state.instance);
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
|
@ -195,6 +199,9 @@ interface ISoapboxLoad {
|
||||||
/** Initial data loader. */
|
/** Initial data loader. */
|
||||||
const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const { setAccount, token, baseApiUri } = useAuth();
|
||||||
|
|
||||||
|
useAxiosInterceptors(token, baseApiUri);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
const me = useAppSelector(state => state.me);
|
||||||
const account = useOwnAccount();
|
const account = useOwnAccount();
|
||||||
|
@ -224,7 +231,8 @@ const SoapboxLoad: React.FC<ISoapboxLoad> = ({ children }) => {
|
||||||
|
|
||||||
// Load initial data from the API
|
// Load initial data from the API
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(loadInitial()).then(() => {
|
dispatch(loadInitial()).then((account) => {
|
||||||
|
setAccount(account);
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
setIsLoaded(true);
|
setIsLoaded(true);
|
||||||
|
@ -282,15 +290,17 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
||||||
/** The root React node of the application. */
|
/** The root React node of the application. */
|
||||||
const Soapbox: React.FC = () => {
|
const Soapbox: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<AuthProvider>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<SoapboxHead>
|
<Provider store={store}>
|
||||||
<SoapboxLoad>
|
<SoapboxHead>
|
||||||
<SoapboxMount />
|
<SoapboxLoad>
|
||||||
</SoapboxLoad>
|
<SoapboxMount />
|
||||||
</SoapboxHead>
|
</SoapboxLoad>
|
||||||
|
</SoapboxHead>
|
||||||
|
</Provider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</Provider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { localState } from 'soapbox/reducers/auth';
|
||||||
|
import { parseBaseURL } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null as any);
|
||||||
|
|
||||||
|
interface IAccount {
|
||||||
|
acct: string
|
||||||
|
avatar: string
|
||||||
|
avatar_static: string
|
||||||
|
bot: boolean
|
||||||
|
created_at: string
|
||||||
|
discoverable: boolean
|
||||||
|
display_name: string
|
||||||
|
emojis: string[]
|
||||||
|
fields: any // not sure
|
||||||
|
followers_count: number
|
||||||
|
following_count: number
|
||||||
|
group: boolean
|
||||||
|
header: string
|
||||||
|
header_static: string
|
||||||
|
id: string
|
||||||
|
last_status_at: string
|
||||||
|
location: string
|
||||||
|
locked: boolean
|
||||||
|
note: string
|
||||||
|
statuses_count: number
|
||||||
|
url: string
|
||||||
|
username: string
|
||||||
|
verified: boolean
|
||||||
|
website: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthProvider: React.FC<any> = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const [account, setAccount] = useState<IAccount>();
|
||||||
|
const [token, setToken] = useState<string>();
|
||||||
|
const [baseApiUri, setBaseApiUri] = useState<string>();
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
account,
|
||||||
|
baseApiUri,
|
||||||
|
setAccount,
|
||||||
|
token,
|
||||||
|
}), [account]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cachedAuth: any = localState?.toJS();
|
||||||
|
|
||||||
|
if (cachedAuth?.me) {
|
||||||
|
setToken(cachedAuth.users[cachedAuth.me].access_token);
|
||||||
|
setBaseApiUri(parseBaseURL(cachedAuth.users[cachedAuth.me].url));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IAuth {
|
||||||
|
account: IAccount
|
||||||
|
baseApiUri: string
|
||||||
|
setAccount(account: IAccount): void
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAuth = (): IAuth => useContext(AuthContext);
|
|
@ -2,9 +2,9 @@ import classNames from 'classnames';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { fetchCarouselAvatars } from 'soapbox/actions/carousels';
|
|
||||||
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
||||||
import { useAppDispatch, useAppSelector, useDimensions, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useDimensions, useFeatures } from 'soapbox/hooks';
|
||||||
|
import useCarouselAvatars from 'soapbox/queries/carousels';
|
||||||
|
|
||||||
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
|
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
|
||||||
import PlaceholderAvatar from '../placeholder/components/placeholder_avatar';
|
import PlaceholderAvatar from '../placeholder/components/placeholder_avatar';
|
||||||
|
@ -15,10 +15,10 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
||||||
const selectedAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId);
|
const selectedAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId);
|
||||||
const isSelected = avatar.account_id === selectedAccountId;
|
const isSelected = avatar.account_id === selectedAccountId;
|
||||||
|
|
||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
const [isFetching, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (isLoading) {
|
if (isFetching) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-disabled={isLoading} onClick={handleClick} className='cursor-pointer' role='filter-feed-by-user'>
|
<div aria-disabled={isFetching} onClick={handleClick} className='cursor-pointer' role='filter-feed-by-user'>
|
||||||
<Stack className='w-16 h-auto' space={3}>
|
<Stack className='w-16 h-auto' space={3}>
|
||||||
<div className='block mx-auto relative w-14 h-14 rounded-full'>
|
<div className='block mx-auto relative w-14 h-14 rounded-full'>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
|
@ -59,17 +59,15 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const FeedCarousel = () => {
|
const FeedCarousel = () => {
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const { data: avatars, isFetching, isError } = useCarouselAvatars();
|
||||||
|
|
||||||
const [cardRef, setCardRef, { width }] = useDimensions();
|
const [cardRef, setCardRef, { width }] = useDimensions();
|
||||||
|
|
||||||
const [pageSize, setPageSize] = useState<number>(0);
|
const [pageSize, setPageSize] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||||
|
|
||||||
const avatars = useAppSelector((state) => state.carousels.avatars);
|
|
||||||
const isLoading = useAppSelector((state) => state.carousels.isLoading);
|
|
||||||
const hasError = useAppSelector((state) => state.carousels.error);
|
|
||||||
const numberOfPages = Math.ceil(avatars.length / pageSize);
|
const numberOfPages = Math.ceil(avatars.length / pageSize);
|
||||||
const widthPerAvatar = (cardRef?.scrollWidth || 0) / avatars.length;
|
const widthPerAvatar = (cardRef?.scrollWidth || 0) / avatars.length;
|
||||||
|
|
||||||
|
@ -85,17 +83,11 @@ const FeedCarousel = () => {
|
||||||
}
|
}
|
||||||
}, [width, widthPerAvatar]);
|
}, [width, widthPerAvatar]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (features.feedUserFiltering) {
|
|
||||||
dispatch(fetchCarouselAvatars());
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!features.feedUserFiltering) {
|
if (!features.feedUserFiltering) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='lg' data-testid='feed-carousel-error'>
|
<Card variant='rounded' size='lg' data-testid='feed-carousel-error'>
|
||||||
<Text align='center'>
|
<Text align='center'>
|
||||||
|
@ -133,7 +125,7 @@ const FeedCarousel = () => {
|
||||||
style={{ transform: `translateX(-${(currentPage - 1) * 100}%)` }}
|
style={{ transform: `translateX(-${(currentPage - 1) * 100}%)` }}
|
||||||
ref={setCardRef}
|
ref={setCardRef}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isFetching ? (
|
||||||
new Array(pageSize).fill(0).map((_, idx) => (
|
new Array(pageSize).fill(0).map((_, idx) => (
|
||||||
<div className='w-16 text-center' key={idx}>
|
<div className='w-16 text-center' key={idx}>
|
||||||
<PlaceholderAvatar size={56} withText />
|
<PlaceholderAvatar size={56} withText />
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { HotKeys } from 'react-hotkeys';
|
import { HotKeys } from 'react-hotkeys';
|
||||||
|
@ -119,6 +120,7 @@ import { WrappedRoute } from './util/react_router_helpers';
|
||||||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||||
// Without this it ends up in ~8 very commonly used bundles.
|
// Without this it ends up in ~8 very commonly used bundles.
|
||||||
import 'soapbox/components/status';
|
import 'soapbox/components/status';
|
||||||
|
import { queryClient } from 'soapbox/queries/client';
|
||||||
|
|
||||||
const EmptyPage = HomePage;
|
const EmptyPage = HomePage;
|
||||||
|
|
||||||
|
@ -648,51 +650,53 @@ const UI: React.FC = ({ children }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
|
<QueryClientProvider client={queryClient}>
|
||||||
<div ref={node} style={style}>
|
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
|
||||||
<BackgroundShapes />
|
<div ref={node} style={style}>
|
||||||
|
<BackgroundShapes />
|
||||||
|
|
||||||
<div className='z-10 flex flex-col'>
|
<div className='z-10 flex flex-col'>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<Layout.Sidebar>
|
<Layout.Sidebar>
|
||||||
{!standalone && <SidebarNavigation />}
|
{!standalone && <SidebarNavigation />}
|
||||||
</Layout.Sidebar>
|
</Layout.Sidebar>
|
||||||
|
|
||||||
<SwitchingColumnsArea>
|
<SwitchingColumnsArea>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
{me && floatingActionButton}
|
{me && floatingActionButton}
|
||||||
|
|
||||||
<BundleContainer fetchComponent={UploadArea}>
|
<BundleContainer fetchComponent={UploadArea}>
|
||||||
{Component => <Component active={draggingOver} onClose={closeUploadModal} />}
|
{Component => <Component active={draggingOver} onClose={closeUploadModal} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
|
|
||||||
{me && (
|
{me && (
|
||||||
<BundleContainer fetchComponent={SidebarMenu}>
|
<BundleContainer fetchComponent={SidebarMenu}>
|
||||||
|
{Component => <Component />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
{me && features.chats && !mobile && (
|
||||||
|
<BundleContainer fetchComponent={ChatPanes}>
|
||||||
|
{Component => <Component />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
<ThumbNavigation />
|
||||||
|
|
||||||
|
<BundleContainer fetchComponent={ProfileHoverCard}>
|
||||||
{Component => <Component />}
|
{Component => <Component />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
|
||||||
{me && features.chats && !mobile && (
|
<BundleContainer fetchComponent={StatusHoverCard}>
|
||||||
<BundleContainer fetchComponent={ChatPanes}>
|
|
||||||
{Component => <Component />}
|
{Component => <Component />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
</div>
|
||||||
<ThumbNavigation />
|
|
||||||
|
|
||||||
<BundleContainer fetchComponent={ProfileHoverCard}>
|
|
||||||
{Component => <Component />}
|
|
||||||
</BundleContainer>
|
|
||||||
|
|
||||||
<BundleContainer fetchComponent={StatusHoverCard}>
|
|
||||||
{Component => <Component />}
|
|
||||||
</BundleContainer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</HotKeys>
|
||||||
</HotKeys>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import API from './client';
|
||||||
|
|
||||||
|
type Avatar = {
|
||||||
|
account_id: string
|
||||||
|
account_avatar: string
|
||||||
|
username: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCarouselAvatars = async () => {
|
||||||
|
const { data } = await API.get('/api/v1/truth/carousels/avatars');
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useCarouselAvatars(): { data: Avatar[], isFetching: boolean, isError: boolean, isSuccess: boolean } {
|
||||||
|
const { data, isFetching, isError, isSuccess } = useQuery<Avatar[]>(['carouselAvatars'], getCarouselAvatars, {
|
||||||
|
placeholderData: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const avatars = data as Avatar[];
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: avatars,
|
||||||
|
isFetching,
|
||||||
|
isError,
|
||||||
|
isSuccess,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,4 +1,20 @@
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
|
||||||
|
import * as BuildConfig from 'soapbox/build_config';
|
||||||
|
import { isURL } from 'soapbox/utils/auth';
|
||||||
|
|
||||||
|
const maybeParseJSON = (data: string) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(data);
|
||||||
|
} catch (Exception) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const API = axios.create({
|
||||||
|
transformResponse: [maybeParseJSON],
|
||||||
|
});
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
|
@ -10,4 +26,22 @@ const queryClient = new QueryClient({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export { queryClient };
|
const useAxiosInterceptors = (token: string, baseApiUri: string) => {
|
||||||
|
API.interceptors.request.use(
|
||||||
|
async(config: AxiosRequestConfig) => {
|
||||||
|
if (token) {
|
||||||
|
config.baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : baseApiUri;
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
config.headers = {
|
||||||
|
...config.headers,
|
||||||
|
Authorization: (token ? `Bearer ${token}` : null) as string | number | boolean | string[] | undefined,
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { API as default, queryClient, useAxiosInterceptors };
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CAROUSEL_AVATAR_REQUEST,
|
|
||||||
CAROUSEL_AVATAR_SUCCESS,
|
|
||||||
CAROUSEL_AVATAR_FAIL,
|
|
||||||
} from 'soapbox/actions/carousels';
|
|
||||||
|
|
||||||
import reducer from '../carousels';
|
|
||||||
|
|
||||||
describe('carousels reducer', () => {
|
|
||||||
it('should return the initial state', () => {
|
|
||||||
expect(reducer(undefined, {} as AnyAction)).toEqual({
|
|
||||||
avatars: [],
|
|
||||||
error: false,
|
|
||||||
isLoading: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('CAROUSEL_AVATAR_REQUEST', () => {
|
|
||||||
it('sets "isLoading" to "true"', () => {
|
|
||||||
const initialState = { isLoading: false, avatars: [], error: false };
|
|
||||||
const action = { type: CAROUSEL_AVATAR_REQUEST };
|
|
||||||
expect(reducer(initialState, action).isLoading).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('CAROUSEL_AVATAR_SUCCESS', () => {
|
|
||||||
it('sets the next state', () => {
|
|
||||||
const initialState = { isLoading: true, avatars: [], error: false };
|
|
||||||
const action = { type: CAROUSEL_AVATAR_SUCCESS, payload: [45] };
|
|
||||||
const result = reducer(initialState, action);
|
|
||||||
|
|
||||||
expect(result.isLoading).toEqual(false);
|
|
||||||
expect(result.avatars).toEqual([45]);
|
|
||||||
expect(result.error).toEqual(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('CAROUSEL_AVATAR_FAIL', () => {
|
|
||||||
it('sets "isLoading" to "true"', () => {
|
|
||||||
const initialState = { isLoading: true, avatars: [], error: false };
|
|
||||||
const action = { type: CAROUSEL_AVATAR_FAIL };
|
|
||||||
const result = reducer(initialState, action);
|
|
||||||
|
|
||||||
expect(result.isLoading).toEqual(false);
|
|
||||||
expect(result.error).toEqual(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -38,7 +38,7 @@ const getSessionUser = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionUser = getSessionUser();
|
const sessionUser = getSessionUser();
|
||||||
const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY)));
|
export const localState = fromJS(JSON.parse(localStorage.getItem(STORAGE_KEY)));
|
||||||
|
|
||||||
// Checks if the user has an ID and access token
|
// Checks if the user has an ID and access token
|
||||||
const validUser = user => {
|
const validUser = user => {
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { AnyAction } from 'redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
CAROUSEL_AVATAR_REQUEST,
|
|
||||||
CAROUSEL_AVATAR_SUCCESS,
|
|
||||||
CAROUSEL_AVATAR_FAIL,
|
|
||||||
} from '../actions/carousels';
|
|
||||||
|
|
||||||
type Avatar = {
|
|
||||||
account_id: string
|
|
||||||
account_avatar: string
|
|
||||||
username: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CarouselsState = {
|
|
||||||
avatars: Avatar[]
|
|
||||||
isLoading: boolean
|
|
||||||
error: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: CarouselsState = {
|
|
||||||
avatars: [],
|
|
||||||
isLoading: false,
|
|
||||||
error: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function rules(state: CarouselsState = initialState, action: AnyAction): CarouselsState {
|
|
||||||
switch (action.type) {
|
|
||||||
case CAROUSEL_AVATAR_REQUEST:
|
|
||||||
return { ...state, isLoading: true };
|
|
||||||
case CAROUSEL_AVATAR_SUCCESS:
|
|
||||||
return { ...state, isLoading: false, avatars: action.payload };
|
|
||||||
case CAROUSEL_AVATAR_FAIL:
|
|
||||||
return { ...state, isLoading: false, error: true };
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ import aliases from './aliases';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import backups from './backups';
|
import backups from './backups';
|
||||||
import carousels from './carousels';
|
|
||||||
import chat_message_lists from './chat_message_lists';
|
import chat_message_lists from './chat_message_lists';
|
||||||
import chat_messages from './chat_messages';
|
import chat_messages from './chat_messages';
|
||||||
import chats from './chats';
|
import chats from './chats';
|
||||||
|
@ -124,7 +123,6 @@ const reducers = {
|
||||||
onboarding,
|
onboarding,
|
||||||
rules,
|
rules,
|
||||||
history,
|
history,
|
||||||
carousels,
|
|
||||||
announcements,
|
announcements,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Ładowanie…
Reference in New Issue