1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

removed redux completely

This commit is contained in:
Fedor Katurov 2022-01-09 19:03:01 +07:00
parent 26e6d8d41b
commit a4bb07e9cf
323 changed files with 2464 additions and 3348 deletions

20
src/hooks/auth/useAuth.ts Normal file
View file

@ -0,0 +1,20 @@
import { useUser } from '~/hooks/auth/useUser';
import { useAuthStore } from '~/store/auth/useAuthStore';
import { useLoginLogoutRestore } from '~/hooks/auth/useLoginLogoutRestore';
export const useAuth = () => {
const { user } = useUser();
const auth = useAuthStore();
const { login, logout } = useLoginLogoutRestore();
return {
user,
logout,
login,
isUser: auth.isUser,
setToken: auth.setToken,
isTester: auth.isTester,
setIsTester: auth.setIsTester,
};
};

View file

@ -0,0 +1,16 @@
import { useCallback } from 'react';
import { useUser } from '~/hooks/auth/useUser';
export const useLastSeenBoris = () => {
const { user, update } = useUser();
const lastSeen = user.last_seen_boris;
const setLastSeen = useCallback(
async (date: string) => {
await update({ last_seen_boris: date }, false);
},
[update]
);
return { setLastSeen, lastSeen };
};

View file

@ -0,0 +1,48 @@
import { Asserts, object, string } from 'yup';
import { ERRORS } from '~/constants/errors';
import { useCallback } from 'react';
import { FormikConfig, useFormik } from 'formik';
import { IUser } from '~/types/auth';
import { showToastSuccess } from '~/utils/toast';
import { getRandomPhrase } from '~/constants/phrases';
import { showErrorToast } from '~/utils/errors/showToast';
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
const validationSchema = object({
username: string().required(ERRORS.REQUIRED),
password: string().required(ERRORS.REQUIRED),
});
export type LoginFormData = Asserts<typeof validationSchema>;
export const useLoginForm = (
fetcher: (username: string, password: string) => Promise<IUser>,
onSuccess: () => void
) => {
const onSubmit = useCallback<FormikConfig<LoginFormData>['onSubmit']>(
async (values, { setErrors }) => {
try {
await fetcher(values.username, values.password);
onSuccess();
showToastSuccess(getRandomPhrase('WELCOME'));
} catch (error) {
showErrorToast(error);
const validationErrors = getValidationErrors(error);
if (validationErrors) {
setErrors(validationErrors);
}
}
},
[fetcher, onSuccess]
);
return useFormik({
validationSchema,
onSubmit,
initialValues: {
username: '',
password: '',
},
});
};

View file

@ -0,0 +1,20 @@
import { useAuthStore } from '~/store/auth/useAuthStore';
import { useCallback } from 'react';
import { apiUserLogin } from '~/api/auth';
export const useLoginLogoutRestore = () => {
const auth = useAuthStore();
const logout = useCallback(() => auth.logout(), [auth]);
const login = useCallback(
async (username: string, password: string) => {
const result = await apiUserLogin({ username, password });
auth.setToken(result.token);
return result.user;
},
[auth]
);
return { logout, login };
};

View file

@ -0,0 +1,53 @@
import { useEffect } from 'react';
import { EventMessageType } from '~/constants/events';
import { includes, path, values } from 'ramda';
import { useOAuth } from '~/hooks/auth/useOAuth';
import { Dialog } from '~/constants/modal';
import { useModal } from '~/hooks/modal/useModal';
import { useAuth } from '~/hooks/auth/useAuth';
/** reacts to events passed by window.postMessage */
export const useMessageEventReactions = () => {
const { loginWithSocial, createSocialAccount, attachAccount } = useOAuth();
const { showModal } = useModal();
const { isUser } = useAuth();
useEffect(() => {
const onMessage = (event: MessageEvent) => {
const type: EventMessageType | undefined = path(['data', 'type'], event);
if (!type || !includes(type, values(EventMessageType))) {
return;
}
console.log('caught event:', type);
switch (type) {
case EventMessageType.OAuthLogin:
// TODO: do we really need it?
loginWithSocial(path(['data', 'payload', 'token'], event));
break;
case EventMessageType.OAuthProcessed:
if (isUser) {
void attachAccount(path(['data', 'payload', 'token'], event));
} else {
void createSocialAccount(path(['data', 'payload', 'token'], event));
}
break;
case EventMessageType.OpenProfile:
const username: string | undefined = path(['data', 'username'], event);
if (!username) {
return;
}
showModal(Dialog.Profile, { username });
break;
default:
console.log('unknown message', event.data);
}
};
window.addEventListener('message', onMessage);
return () => window.removeEventListener('message', onMessage);
}, [attachAccount, createSocialAccount, loginWithSocial, isUser, showModal]);
};

101
src/hooks/auth/useOAuth.ts Normal file
View file

@ -0,0 +1,101 @@
import { useCallback } from 'react';
import { OAuthProvider } from '~/types/auth';
import { API } from '~/constants/api';
import { apiAttachSocial, apiDropSocial, apiGetSocials, apiLoginWithSocial } from '~/api/auth';
import { useModal } from '~/hooks/modal/useModal';
import { Dialog } from '~/constants/modal';
import { showErrorToast } from '~/utils/errors/showToast';
import { path } from 'ramda';
import useSWR from 'swr';
import { useAuth } from '~/hooks/auth/useAuth';
export const useOAuth = () => {
const { isUser, setToken } = useAuth();
const { showModal, hideModal } = useModal();
const { data, isValidating: isLoading, mutate } = useSWR(
isUser ? API.USER.GET_SOCIALS : null,
async () => {
const result = await apiGetSocials();
return result.accounts;
}
);
const openOauthWindow = useCallback((provider: OAuthProvider) => {
window.open(API.USER.OAUTH_WINDOW(provider), '', 'width=600,height=400');
}, []);
const createSocialAccount = useCallback(
async (token?: string) => {
try {
if (!token) return;
const result = await apiLoginWithSocial({ token });
setToken(result.token);
hideModal();
} catch (error) {
const needsRegister: string | undefined = path(
['response', 'data', 'needs_register'],
error
);
if (needsRegister && token) {
showModal(Dialog.LoginSocialRegister, { token });
return;
}
showErrorToast(error);
}
},
[showModal, hideModal, setToken]
);
const loginWithSocial = useCallback(
(token: string | undefined) => {
if (!token) {
return;
}
setToken(token);
hideModal();
},
[setToken, hideModal]
);
const attachAccount = useCallback(
async (token?: string) => {
try {
if (!token) return;
await apiAttachSocial({ token });
await mutate();
} catch (error) {
showErrorToast(error);
}
},
[mutate]
);
const dropAccount = useCallback(
async (provider: OAuthProvider, id: string) => {
try {
await apiDropSocial({ id, provider });
await mutate();
} catch (error) {
showErrorToast(error);
}
},
[mutate]
);
return {
openOauthWindow,
loginWithSocial,
createSocialAccount,
attachAccount,
dropAccount,
accounts: data || [],
isLoading: !data && isLoading,
};
};

View file

@ -2,32 +2,34 @@ import { useUploader } from '~/hooks/data/useUploader';
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { useCallback } from 'react';
import { showErrorToast } from '~/utils/errors/showToast';
import { ApiUpdateUserRequest, IUser } from '~/redux/auth/types';
import { apiUpdateUser } from '~/redux/auth/api';
import { apiUpdateUser } from '~/api/auth';
import { ApiUpdateUserRequest } from '~/api/auth/types';
import { useUser } from '~/hooks/auth/useUser';
export const usePatchProfile = (updateUserData: (user: Partial<IUser>) => void) => {
export const usePatchUser = () => {
const { update } = useUser();
const { uploadFile } = useUploader(UploadSubject.Avatar, UploadTarget.Profiles);
const updateProfile = useCallback(
const save = useCallback(
async (user: Partial<ApiUpdateUserRequest['user']>) => {
const result = await apiUpdateUser({ user });
await updateUserData(result.user);
await update(result.user);
return result.user;
},
[updateUserData]
[update]
);
const updatePhoto = useCallback(
async (file: File) => {
try {
const photo = await uploadFile(file);
await updateProfile({ photo });
await save({ photo });
} catch (error) {
showErrorToast(error);
}
},
[uploadFile, updateProfile]
[uploadFile, save]
);
return { updatePhoto, updateProfile };
return { updatePhoto, save };
};

View file

@ -0,0 +1,14 @@
import { apiCheckRestoreCode } from '~/api/auth';
import useSWR from 'swr';
import { API } from '~/constants/api';
import { getErrorMessage } from '~/utils/errors/getErrorMessage';
export const useRestoreCode = (code: string) => {
const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () =>
apiCheckRestoreCode({ code })
);
const codeUser = data?.user;
return { codeUser, isLoading: isValidating, error: getErrorMessage(error) };
};

View file

@ -0,0 +1,56 @@
import { Asserts, object, string } from 'yup';
import { FormikConfig, useFormik } from 'formik';
import { useCallback } from 'react';
import { showErrorToast } from '~/utils/errors/showToast';
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
import { ERRORS } from '~/constants/errors';
import { IUser } from '~/types/auth';
import { useAuthStore } from '~/store/auth/useAuthStore';
const validationSchema = object({
newPassword: string()
.required(ERRORS.REQUIRED)
.min(6, ERRORS.PASSWORD_IS_SHORT),
newPasswordAgain: string()
.required(ERRORS.REQUIRED)
.test(
'sameAsPassword',
'Должен совпадать с паролем',
(val, ctx) => val === ctx.parent.newPassword
),
});
export type RestorePasswordData = Asserts<typeof validationSchema>;
export const useRestorePasswordForm = (
code: string,
fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>,
onSuccess: () => void
) => {
const auth = useAuthStore();
const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>(
async (values, { setErrors }) => {
try {
const { token, user } = await fetcher({ password: values.newPassword, code });
auth.setUser(user);
auth.setToken(token);
onSuccess();
} catch (error) {
showErrorToast(error);
const validationErrors = getValidationErrors(error);
if (validationErrors) {
setErrors(validationErrors);
}
}
},
[onSuccess, fetcher, code, auth]
);
return useFormik<RestorePasswordData>({
onSubmit,
validationSchema,
initialValues: { newPassword: '', newPasswordAgain: '' },
});
};

View file

@ -0,0 +1,21 @@
import { useEffect } from 'react';
import { useHistory } from 'react-router';
import { useModal } from '~/hooks/modal/useModal';
import { Dialog } from '~/constants/modal';
/** redirects to the password redirect modal */
export const useRestorePasswordRedirect = () => {
const history = useHistory();
const { showModal } = useModal();
useEffect(() => {
const match = window.location.pathname.match(/^\/restore\/([\w\d-]+)$/);
if (!match?.[1]) {
return;
}
history.push('/');
showModal(Dialog.RestorePassword, { code: match[1] });
}, [showModal, history]);
};

View file

@ -0,0 +1,40 @@
import { Asserts, object, string } from 'yup';
import { ERRORS } from '~/constants/errors';
import { FormikConfig, useFormik } from 'formik';
import { useCallback } from 'react';
import { showErrorToast } from '~/utils/errors/showToast';
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
const validationSchema = object({
field: string().required(ERRORS.REQUIRED),
});
type RestoreRequestData = Asserts<typeof validationSchema>;
export const useRestoreRequestForm = (
fetcher: (field: string) => Promise<unknown>,
onSuccess: () => void
) => {
const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>(
async (values, { setErrors }) => {
try {
await fetcher(values.field);
onSuccess();
} catch (error) {
showErrorToast(error);
const validationErrors = getValidationErrors(error);
if (validationErrors) {
setErrors(validationErrors);
}
}
},
[fetcher, onSuccess]
);
return useFormik({
onSubmit,
validationSchema,
initialValues: { field: '' },
});
};

View file

@ -0,0 +1,55 @@
import { Asserts, object, string } from 'yup';
import { ERRORS } from '~/constants/errors';
import { FormikConfig, useFormik } from 'formik';
import { useCallback } from 'react';
import { showErrorToast } from '~/utils/errors/showToast';
import { getValidationErrors } from '~/utils/errors/getValidationErrors';
const validationSchema = object({
username: string().required(ERRORS.REQUIRED),
password: string()
.required(ERRORS.REQUIRED)
.min(6, ERRORS.PASSWORD_IS_SHORT),
});
type SocialRegisterData = Asserts<typeof validationSchema>;
export const useSocialRegisterForm = (
token: string,
fetcher: (props: {
token: string;
username: string;
password: string;
}) => Promise<{ token: string }>,
onSuccess: (token: string) => void
) => {
const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>(
async (values, { setErrors }) => {
try {
const result = await fetcher({
token,
username: values.username,
password: values.password || '',
});
onSuccess(result.token);
} catch (error) {
showErrorToast(error);
const validationErrors = getValidationErrors(error);
if (validationErrors) {
setErrors(validationErrors);
}
}
},
[token, onSuccess, fetcher]
);
return useFormik<SocialRegisterData>({
validationSchema,
onSubmit,
initialValues: {
username: '',
password: '',
},
});
};

30
src/hooks/auth/useUser.ts Normal file
View file

@ -0,0 +1,30 @@
import useSWR from 'swr';
import { useAuthStore } from '~/store/auth/useAuthStore';
import { API } from '~/constants/api';
import { apiAuthGetUser } from '~/api/auth';
import { EMPTY_USER } from '~/constants/auth';
import { showErrorToast } from '~/utils/errors/showToast';
import { useCallback } from 'react';
import { IUser } from '~/types/auth';
export const useUser = () => {
const { token, setUser } = useAuthStore();
const { data, mutate } = useSWR(token ? API.USER.ME : null, () => apiAuthGetUser(), {
onSuccess: data => setUser(data?.user || EMPTY_USER),
onError: error => showErrorToast(error),
});
const update = useCallback(
async (user: Partial<IUser>, revalidate?: boolean) => {
if (!data?.user) {
console.warn('mutating user before loaded it :-(');
return;
}
await mutate({ ...data, user: { ...data.user, ...user } }, revalidate);
},
[data, mutate]
);
return { user: data?.user || EMPTY_USER, update };
};

View file

@ -1,4 +1,4 @@
import { IUser } from '~/redux/auth/types';
import { IUser } from '~/types/auth';
import { useRandomPhrase } from '~/constants/phrases';
import { differenceInDays, parseISO } from 'date-fns';
import { INACTIVE_ACCOUNT_DAYS } from '~/constants/user';

View file

@ -1,17 +1,16 @@
import { useDispatch } from 'react-redux';
import { useCallback, useEffect } from 'react';
import isBefore from 'date-fns/isBefore';
import { authSetState, authSetUser } from '~/redux/auth/actions';
import { useUser } from '~/hooks/user/userUser';
import { IComment } from '~/redux/types';
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
import { selectAuthIsTester } from '~/redux/auth/selectors';
import { IComment } from '~/types';
import { useRandomPhrase } from '~/constants/phrases';
import { useBorisStats } from '~/hooks/boris/useBorisStats';
import { useLastSeenBoris } from '~/hooks/auth/useLastSeenBoris';
import { useAuth } from '~/hooks/auth/useAuth';
export const useBoris = (comments: IComment[]) => {
const dispatch = useDispatch();
const user = useUser();
const title = useRandomPhrase('BORIS_TITLE');
const { lastSeen, setLastSeen } = useLastSeenBoris();
const { isTester, setIsTester } = useAuth();
useEffect(() => {
const last_comment = comments[0];
@ -19,26 +18,24 @@ export const useBoris = (comments: IComment[]) => {
if (!last_comment) return;
if (
user.last_seen_boris &&
last_comment.created_at &&
!isBefore(new Date(user.last_seen_boris), new Date(last_comment.created_at))
)
!last_comment.created_at ||
!lastSeen ||
isBefore(new Date(lastSeen), new Date(last_comment.created_at))
) {
return;
}
dispatch(authSetUser({ last_seen_boris: last_comment.created_at }));
}, [user.last_seen_boris, dispatch, comments]);
void setLastSeen(last_comment.created_at);
}, [lastSeen, setLastSeen, comments]);
const { stats, isLoading: isLoadingStats } = useBorisStats();
const setIsBetaTester = useCallback(
(is_tester: boolean) => {
dispatch(authSetState({ is_tester }));
(isTester: boolean) => {
setIsTester(isTester);
},
[dispatch]
[setIsTester]
);
const isTester = useShallowSelect(selectAuthIsTester);
const title = useRandomPhrase('BORIS_TITLE');
return { setIsBetaTester, isTester, stats, title, isLoadingStats };
};

View file

@ -10,9 +10,7 @@ export const useBorisStats = () => {
() => getBorisBackendStats()
);
const { data: issues = [], isValidating: isValidatingGit } = useSWR(API.BORIS.GITHUB_ISSUES, () =>
getGithubIssues()
);
const { data: issues = [] } = useSWR(API.BORIS.GITHUB_ISSUES, () => getGithubIssues());
const stats: BorisUsageStats = {
backend,

View file

@ -1,4 +1,4 @@
import { IComment, INode } from '~/redux/types';
import { IComment, INode } from '~/types';
import { useCallback, useEffect, useRef } from 'react';
import { FormikHelpers, useFormik, useFormikContext } from 'formik';
import { array, object, string } from 'yup';

View file

@ -4,7 +4,7 @@ import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
import { apiGetNodeComments } from '~/api/node';
import { COMMENTS_DISPLAY } from '~/constants/node';
import { useCallback } from 'react';
import { IComment } from '~/redux/types';
import { IComment } from '~/types';
const getKey: (nodeId: number) => SWRInfiniteKeyLoader = (nodeId: number) => (
pageIndex,

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { IComment } from '~/redux/types';
import { IComment } from '~/types';
import { useGetComments } from '~/hooks/comments/useGetComments';
import { apiLockComment, apiPostComment } from '~/api/node';
import { showErrorToast } from '~/utils/errors/showToast';

View file

@ -7,13 +7,13 @@ export const usePersistedState = (key: string, initial: string): [string, (val:
} catch (e) {
return initial;
}
}, [key]);
}, [key, initial]);
const [val, setVal] = useState<string>(stored);
useEffect(() => {
localStorage.setItem(`vault_${key}`, val);
}, [val]);
}, [val, key]);
return [val, setVal];
};

View file

@ -1,5 +0,0 @@
import { shallowEqual, useSelector } from 'react-redux';
import { IState } from '~/redux/store';
export const useShallowSelect = <T extends (state: IState) => any>(selector: T): ReturnType<T> =>
useSelector(selector, shallowEqual);

View file

@ -1,5 +1,5 @@
import { UploadSubject, UploadTarget } from '~/constants/uploads';
import { IFile } from '~/redux/types';
import { IFile } from '~/types';
import { useCallback } from 'react';
import { apiUploadFile } from '~/api/uploads';
import { keys } from 'ramda';

View file

@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback } from 'react';
/** wraps text inside textarea with tags */
export const useFormatWrapper = (onChange: (val: string) => void) => {

View file

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useFlowStore } from "~/store/flow/useFlowStore";
import { hideLoader } from "~/utils/dom/hideLoader";
import { useFlowLoader } from "~/hooks/flow/useFlowLoader";
import { useEffect } from 'react';
import { useFlowStore } from '~/store/flow/useFlowStore';
import { hideLoader } from '~/utils/dom/hideLoader';
import { useFlowLoader } from '~/hooks/flow/useFlowLoader';
/** simply waits for all data to settle and then show the app */
export const useGlobalLoader = () => {

View file

@ -1,5 +1,5 @@
import { useCallback } from "react";
import { getImageFromPaste } from "~/utils/uploader";
import { useCallback } from 'react';
import { getImageFromPaste } from '~/utils/uploader';
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
export const useInputPasteUpload = (onUpload: (files: File[]) => void) => {

View file

@ -2,18 +2,21 @@ import { useEffect } from 'react';
import { NEW_COMMENT_CLASSNAME } from '~/constants/comment';
export const useScrollToTop = (deps?: any[]) => {
useEffect(() => {
const targetElement = document.querySelector(`.${NEW_COMMENT_CLASSNAME}`);
useEffect(
() => {
const targetElement = document.querySelector(`.${NEW_COMMENT_CLASSNAME}`);
if (!targetElement) {
window.scrollTo(0, 0);
return;
}
if (!targetElement) {
window.scrollTo(0, 0);
return;
}
const bounds = targetElement.getBoundingClientRect();
window.scrollTo({
top: bounds.top - 100,
behavior: 'smooth',
});
}, deps || []);
const bounds = targetElement.getBoundingClientRect();
window.scrollTo({
top: bounds.top - 100,
behavior: 'smooth',
});
},
deps && Array.isArray(deps) ? deps : []
);
};

View file

@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
export const useScrollTop = () => {
const [top, setTop] = useState(0);
useEffect(() => {
const onScroll = () => {
setTop(window.scrollY);
};
window.addEventListener('scroll', onScroll);
return () => window.removeEventListener('scroll', onScroll);
}, []);
return top;
};

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { FlowDisplay, INode } from '~/redux/types';
import { FlowDisplay, INode } from '~/types';
export const useFlowCellControls = (
id: INode['id'],

View file

@ -1,6 +1,6 @@
import { useCallback } from "react";
import { usePersistedState } from "~/hooks/data/usePersistedState";
import { experimentalFeatures } from "~/constants/features";
import { useCallback } from 'react';
import { usePersistedState } from '~/hooks/data/usePersistedState';
import { experimentalFeatures } from '~/constants/features';
enum Layout {
Fluid = 'fluid',

View file

@ -4,7 +4,6 @@ import { uniq } from 'ramda';
import { useFlowStore } from '~/store/flow/useFlowStore';
import { runInAction } from 'mobx';
import { showErrorToast } from '~/utils/errors/showToast';
import { delay } from 'redux-saga/effects';
export const useFlowLoader = () => {
const [isSyncing, setIsSyncing] = useState(false);
@ -74,7 +73,7 @@ export const useFlowLoader = () => {
});
// wait a little to debounce
await delay(1000);
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
showErrorToast(error);
} finally {

View file

@ -1,6 +1,6 @@
import { useFlowStore } from '~/store/flow/useFlowStore';
import { useCallback } from 'react';
import { FlowDisplay } from '~/redux/types';
import { FlowDisplay } from '~/types';
import { showErrorToast } from '~/utils/errors/showToast';
import { postCellView } from '~/api/flow';

View file

@ -3,9 +3,9 @@ import { GetLabNodesRequest, ILabNode } from '~/types/lab';
import { getLabNodes } from '~/api/lab';
import { flatten, last, uniqBy } from 'ramda';
import { useLabStore } from '~/store/lab/useLabStore';
import { useCallback, useEffect } from 'react';
import { INode } from '~/redux/types';
import { useUser } from '~/hooks/user/userUser';
import { useCallback } from 'react';
import { INode } from '~/types';
import { useAuth } from '~/hooks/auth/useAuth';
const getKey: (isUser: boolean) => SWRInfiniteKeyLoader = isUser => (index, prev: ILabNode[]) => {
if (!isUser) return null;
@ -33,10 +33,10 @@ const parseKey = (key: string): GetLabNodesRequest => {
export const useGetLabNodes = () => {
const labStore = useLabStore();
const { is_user } = useUser();
const { isUser } = useAuth();
const { data, isValidating, size, setSize, mutate } = useSWRInfinite(
getKey(is_user),
getKey(isUser),
async (key: string) => {
const result = await getLabNodes(parseKey(key));
return result.nodes;
@ -71,14 +71,5 @@ export const useGetLabNodes = () => {
[data, mutate]
);
/** purge cache on exit */
useEffect(() => {
if (is_user) {
return;
}
labStore.setNodes([]);
}, [is_user, labStore]);
return { nodes, isLoading: !data && isValidating, hasMore, loadMore, unshift, updateNode };
};

View file

@ -2,17 +2,17 @@ import useSWR from 'swr';
import { API } from '~/constants/api';
import { getLabStats, getLabUpdates } from '~/api/lab';
import { useLabStore } from '~/store/lab/useLabStore';
import { useCallback, useEffect } from 'react';
import { useUser } from '~/hooks/user/userUser';
import { useCallback, useMemo } from 'react';
import { useAuth } from '~/hooks/auth/useAuth';
const refreshInterval = 1000 * 60 * 5; // 5 minutes
export const useGetLabStats = () => {
const lab = useLabStore();
const { is_user } = useUser();
const { isUser } = useAuth();
const { data: stats, isValidating: isValidatingStats } = useSWR(
is_user ? API.LAB.STATS : null,
isUser ? API.LAB.STATS : null,
async () => getLabStats(),
{
fallbackData: {
@ -28,7 +28,7 @@ export const useGetLabStats = () => {
);
const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR(
is_user ? API.LAB.UPDATES : null,
isUser ? API.LAB.UPDATES : null,
async () => {
const result = await getLabUpdates();
return result.nodes;
@ -42,9 +42,9 @@ export const useGetLabStats = () => {
}
);
const heroes = stats?.heroes || [];
const tags = stats?.tags || [];
const updates = updatesData || [];
const heroes = useMemo(() => stats?.heroes || [], [stats]);
const tags = useMemo(() => stats?.tags || [], [stats]);
const updates = useMemo(() => updatesData || [], [updatesData]);
const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates);
const seenNode = useCallback(
@ -57,16 +57,5 @@ export const useGetLabStats = () => {
[mutateUpdates, updates]
);
/** purge cache on exit */
useEffect(() => {
if (is_user) {
return;
}
lab.setHeroes([]);
lab.setTags([]);
lab.setUpdates([]);
}, [is_user, lab]);
return { heroes, tags, updates, isLoading, seenNode };
};

View file

@ -1,7 +1,7 @@
import useSWR from 'swr';
import { API } from '~/constants/api';
import { apiGetUserMessages } from '~/api/messages';
import { IMessage } from '~/redux/types';
import { IMessage } from '~/types';
const getKey = (username: string): string | null => {
return username ? `${API.USER.MESSAGES}/${username}` : null;

View file

@ -10,6 +10,7 @@ export const useYoutubeMetadata = (id: string) => {
}
enqueue(id);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return metadata[id];

View file

@ -1,24 +0,0 @@
import { useEffect } from "react";
import { history } from "~/redux/store";
/**
* useBlockBackButton - blocks back navigation and calls {callback}
* @param callback
*/
export const useBlockBackButton = (callback?: () => void) => {
useEffect(
() =>
history.listen((newLocation, action) => {
if (action !== 'POP') {
return;
}
history.goForward();
if (callback) {
callback();
}
}),
[callback]
);
};

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { IFile } from '~/redux/types';
import { IFile } from '~/types';
import { useShowModal } from '~/hooks/modal/useShowModal';
import { Dialog } from '~/constants/modal';

View file

@ -1,5 +1,5 @@
import { useCallback } from 'react';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { apiPostNode } from '~/api/node';
import { useFlowStore } from '~/store/flow/useFlowStore';
import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import useSWR from 'swr';
import { ApiGetNodeRelatedResult } from '~/types/node';
import { API } from '~/constants/api';

View file

@ -1,7 +1,7 @@
import { INode } from "~/redux/types";
import { useHistory } from "react-router";
import { useCallback } from "react";
import { URLS } from "~/constants/urls";
import { INode } from '~/types';
import { useHistory } from 'react-router';
import { useCallback } from 'react';
import { URLS } from '~/constants/urls';
// useGotoNode returns fn, that navigates to node
export const useGotoNode = (id: INode['id']) => {

View file

@ -1,4 +1,4 @@
import { IComment } from '~/redux/types';
import { IComment } from '~/types';
import { useMemo } from 'react';
import { groupCommentsByUser } from '~/utils/fn';

View file

@ -4,7 +4,7 @@ import { API } from '~/constants/api';
import { useOnNodeSeen } from '~/hooks/node/useOnNodeSeen';
import { apiGetNode } from '~/api/node';
import { useCallback } from 'react';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { EMPTY_NODE } from '~/constants/node';
export const useLoadNode = (id: number) => {

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useCallback } from 'react';
import { apiLockNode, apiPostNodeHeroic, apiPostNodeLike } from '~/api/node';
import { showErrorToast } from '~/utils/errors/showToast';

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useMemo } from 'react';
import { UploadType } from '~/constants/uploads';

View file

@ -1,7 +1,7 @@
import { INode } from "~/redux/types";
import { createElement, FC, useCallback, useMemo } from "react";
import { isNil, prop } from "ramda";
import { INodeComponentProps, LAB_PREVIEW_LAYOUT, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from "~/constants/node";
import { INode } from '~/types';
import { createElement, FC, useCallback, useMemo } from 'react';
import { isNil, prop } from 'ramda';
import { INodeComponentProps, LAB_PREVIEW_LAYOUT, NODE_COMPONENTS, NODE_HEADS, NODE_INLINES } from '~/constants/node';
// useNodeBlocks returns head, block and inline blocks of node
export const useNodeBlocks = (node: INode, isLoading: boolean) => {

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { usePageCover } from '~/components/containers/PageCoverProvider/usePageCover';
export const useNodeCoverImage = (node: INode) => {

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useCallback, useRef } from 'react';
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
import { object } from 'yup';

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useMemo } from 'react';
import { UploadType } from '~/constants/uploads';

View file

@ -1,11 +1,11 @@
import { useMemo } from 'react';
import { canEditNode, canLikeNode, canStarNode } from '~/utils/node';
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
import { selectUser } from '~/redux/auth/selectors';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { useUser } from '~/hooks/auth/useUser';
export const useNodePermissions = (node?: INode) => {
const user = useShallowSelect(selectUser);
const { user } = useUser();
const edit = useMemo(() => canEditNode(node, user), [node, user]);
const like = useMemo(() => canLikeNode(node, user), [node, user]);
const star = useMemo(() => canStarNode(node, user), [node, user]);

View file

@ -1,6 +1,6 @@
import { useHistory } from 'react-router';
import { useCallback } from 'react';
import { ITag } from '~/redux/types';
import { ITag } from '~/types';
import { URLS } from '~/constants/urls';
import { useLoadNode } from '~/hooks/node/useLoadNode';
import { apiDeleteNodeTag, apiPostNodeTags } from '~/api/node';

View file

@ -1,7 +1,7 @@
import { INode } from "~/redux/types";
import { useEffect } from "react";
import { useFlowStore } from "~/store/flow/useFlowStore";
import { useGetLabStats } from "~/hooks/lab/useGetLabStats";
import { INode } from '~/types';
import { useEffect } from 'react';
import { useFlowStore } from '~/store/flow/useFlowStore';
import { useGetLabStats } from '~/hooks/lab/useGetLabStats';
// useOnNodeSeen updates node seen status across all needed places
export const useOnNodeSeen = (node?: INode) => {

View file

@ -1,13 +1,11 @@
import { useLoadNode } from '~/hooks/node/useLoadNode';
import { useCallback } from 'react';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { apiPostNode } from '~/api/node';
import { useDispatch } from 'react-redux';
import { useFlowStore } from '~/store/flow/useFlowStore';
import { useGetLabNodes } from '~/hooks/lab/useGetLabNodes';
export const useUpdateNode = (id: number) => {
const dispatch = useDispatch();
const { update } = useLoadNode(id);
const flow = useFlowStore();
const lab = useGetLabNodes();

View file

@ -1,9 +1,9 @@
import useSWR from 'swr';
import { API } from '~/constants/api';
import { apiAuthGetUserProfile } from '~/redux/auth/api';
import { EMPTY_USER } from '~/redux/auth/constants';
import { apiAuthGetUserProfile } from '~/api/auth';
import { EMPTY_USER } from '~/constants/auth';
import { useCallback } from 'react';
import { IUser } from '~/redux/auth/types';
import { IUser } from '~/types/auth';
const getKey = (username?: string): string | null => {
return username ? `${API.USER.PROFILE}/${username}` : null;

View file

@ -1,4 +1,4 @@
import { IUser } from '~/redux/auth/types';
import { IUser } from '~/types/auth';
import { Asserts, object, string } from 'yup';
import { ERRORS } from '~/constants/errors';
import { FormikConfig, useFormik } from 'formik';

View file

@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';
import { flatten } from 'ramda';
import { getSearchResults } from '~/api/flow';
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { GetSearchResultsRequest } from '~/types/flow';
const RESULTS_COUNT = 20;

View file

@ -1,4 +1,4 @@
import { INode } from '~/redux/types';
import { INode } from '~/types';
import { API } from '~/constants/api';
import { flatten, isNil } from 'ramda';
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite';

View file

@ -0,0 +1,17 @@
import useSWR from 'swr';
import { useAuth } from '~/hooks/auth/useAuth';
import { apiAuthGetUpdates } from '~/api/auth';
import { API } from '~/constants/api';
export const useUpdates = () => {
const { isUser } = useAuth();
const { data } = useSWR(
isUser ? API.USER.GET_UPDATES : null,
() => apiAuthGetUpdates({ exclude_dialogs: 0, last: '' }),
{ refreshInterval: 5 * 60 * 1000 }
);
const borisCommentedAt = data?.boris?.commented_at || '';
return { borisCommentedAt };
};

View file

@ -1,4 +0,0 @@
import { useShallowSelect } from '~/hooks/data/useShallowSelect';
import { selectUser } from '~/redux/auth/selectors';
export const useUser = () => useShallowSelect(selectUser);