Refactor snackbar old component scoped state (#13183)
This PR refactors the snackbar modules that was using legacy versions of our state management. We replace the old states with new ones and also the old scoped context with component instance context.
This commit is contained in:
@ -8,7 +8,7 @@ import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTa
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
@ -112,9 +112,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider cache={cache}>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
|
||||
@ -4,7 +4,7 @@ import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserve
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { AppRootErrorFallback } from '@/error-handler/components/AppRootErrorFallback';
|
||||
import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { I18nProvider } from '@lingui/react';
|
||||
@ -25,7 +25,7 @@ export const App = () => {
|
||||
<I18nProvider i18n={i18n}>
|
||||
<RecoilDebugObserverEffect />
|
||||
<ApolloDevLogEffect />
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
<IconsProvider>
|
||||
<ExceptionHandlerProvider>
|
||||
<HelmetProvider>
|
||||
@ -37,7 +37,7 @@ export const App = () => {
|
||||
</HelmetProvider>
|
||||
</ExceptionHandlerProvider>
|
||||
</IconsProvider>
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</I18nProvider>
|
||||
</AppErrorBoundary>
|
||||
</RecoilRoot>
|
||||
|
||||
@ -2,7 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -14,8 +14,8 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { iconsState } from 'twenty-ui/display';
|
||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||
import { SupportDriver } from '~/generated/graphql';
|
||||
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
|
||||
|
||||
const redirectSpy = jest.fn();
|
||||
|
||||
@ -35,9 +35,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
|
||||
@ -4,7 +4,7 @@ import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
import {
|
||||
query,
|
||||
@ -27,9 +27,9 @@ const mocks = [
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -5,7 +5,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
import { MultipleRecordPickerRecords } from '@/object-record/record-picker/multiple-record-picker/types/MultipleRecordPickerRecords';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
@ -55,9 +55,9 @@ const mocks = [
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -14,7 +14,7 @@ import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
|
||||
import { useSettingsPermissionMap } from '@/settings/roles/hooks/useSettingsPermissionMap';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarComponentInstanceContextProvider } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarComponentInstanceContextProvider';
|
||||
|
||||
const mockCurrentUser = {
|
||||
id: 'fake-user-id',
|
||||
@ -45,9 +45,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider>
|
||||
<RecoilRoot initializeState={initializeState}>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
<SnackBarComponentInstanceContextProvider snackBarComponentInstanceId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</SnackBarComponentInstanceContextProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { snackBarInternalComponentState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState';
|
||||
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
|
||||
import { SnackBar } from './SnackBar';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
|
||||
import { SnackBar } from './SnackBar';
|
||||
|
||||
const StyledSnackBarContainer = styled.div`
|
||||
bottom: ${({ theme }) => theme.spacing(3)};
|
||||
@ -26,7 +27,10 @@ const StyledSnackBarContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const SnackBarProvider = ({ children }: React.PropsWithChildren) => {
|
||||
const { snackBarInternal } = useSnackBarManagerScopedStates();
|
||||
const snackBarInternal = useRecoilComponentValueV2(
|
||||
snackBarInternalComponentState,
|
||||
);
|
||||
|
||||
const { handleSnackBarClose } = useSnackBar();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const SnackBarComponentInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
@ -1,45 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useSnackBarManagerScopedStates } from '@/ui/feedback/snack-bar-manager/hooks/internal/useSnackBarManagerScopedStates';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { SnackBarState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
|
||||
|
||||
const snackBarManagerScopeId = 'snack-bar-manager';
|
||||
|
||||
const Wrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<SnackBarProviderScope snackBarManagerScopeId={snackBarManagerScopeId}>
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useSnackBarManagerScopedStates', () => {
|
||||
it('should return snackbar state and a function to update the state', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useSnackBarManagerScopedStates({
|
||||
snackBarManagerScopeId,
|
||||
}),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const defaultState = { maxQueue: 3, queue: [] };
|
||||
|
||||
expect(result.current.snackBarInternal).toEqual(defaultState);
|
||||
|
||||
const newSnackBarState: SnackBarState = {
|
||||
maxQueue: 5,
|
||||
queue: [{ id: 'testid', role: 'alert', message: 'TEST MESSAGE' }],
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.setSnackBarInternal(newSnackBarState);
|
||||
});
|
||||
|
||||
expect(result.current.snackBarInternal).toEqual(newSnackBarState);
|
||||
});
|
||||
});
|
||||
@ -1,24 +0,0 @@
|
||||
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
|
||||
import { snackBarInternalScopedState } from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
|
||||
import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
|
||||
type useSnackBarManagerScopedStatesProps = {
|
||||
snackBarManagerScopeId?: string;
|
||||
};
|
||||
|
||||
export const useSnackBarManagerScopedStates = (
|
||||
props?: useSnackBarManagerScopedStatesProps,
|
||||
) => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
SnackBarManagerScopeInternalContext,
|
||||
props?.snackBarManagerScopeId,
|
||||
);
|
||||
|
||||
const [snackBarInternal, setSnackBarInternal] = useRecoilScopedStateV2(
|
||||
snackBarInternalScopedState,
|
||||
scopeId,
|
||||
);
|
||||
|
||||
return { snackBarInternal, setSnackBarInternal };
|
||||
};
|
||||
@ -3,59 +3,69 @@ import { useRecoilCallback } from 'recoil';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext';
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
import {
|
||||
snackBarInternalScopedState,
|
||||
snackBarInternalComponentState,
|
||||
SnackBarOptions,
|
||||
} from '@/ui/feedback/snack-bar-manager/states/snackBarInternalScopedState';
|
||||
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
|
||||
} from '@/ui/feedback/snack-bar-manager/states/snackBarInternalComponentState';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { getErrorMessageFromApolloError } from '~/utils/get-error-message-from-apollo-error.util';
|
||||
|
||||
export const useSnackBar = () => {
|
||||
const scopeId = useAvailableScopeIdOrThrow(
|
||||
SnackBarManagerScopeInternalContext,
|
||||
const componentInstanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
SnackBarComponentInstanceContext,
|
||||
);
|
||||
|
||||
const handleSnackBarClose = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(id: string) => {
|
||||
set(snackBarInternalScopedState({ scopeId }), (prevState) => ({
|
||||
...prevState,
|
||||
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
|
||||
}));
|
||||
set(
|
||||
snackBarInternalComponentState.atomFamily({
|
||||
instanceId: componentInstanceId,
|
||||
}),
|
||||
(prevState) => ({
|
||||
...prevState,
|
||||
queue: prevState.queue.filter((snackBar) => snackBar.id !== id),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[scopeId],
|
||||
[componentInstanceId],
|
||||
);
|
||||
|
||||
const setSnackBarQueue = useRecoilCallback(
|
||||
({ set }) =>
|
||||
(newValue: SnackBarOptions) =>
|
||||
set(snackBarInternalScopedState({ scopeId }), (prev) => {
|
||||
if (
|
||||
isDefined(newValue.dedupeKey) &&
|
||||
prev.queue.some(
|
||||
(snackBar) => snackBar.dedupeKey === newValue.dedupeKey,
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
set(
|
||||
snackBarInternalComponentState.atomFamily({
|
||||
instanceId: componentInstanceId,
|
||||
}),
|
||||
(prev) => {
|
||||
if (
|
||||
isDefined(newValue.dedupeKey) &&
|
||||
prev.queue.some(
|
||||
(snackBar) => snackBar.dedupeKey === newValue.dedupeKey,
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
if (prev.queue.length >= prev.maxQueue) {
|
||||
return {
|
||||
...prev,
|
||||
queue: [...prev.queue.slice(1), newValue] as SnackBarOptions[],
|
||||
};
|
||||
}
|
||||
|
||||
if (prev.queue.length >= prev.maxQueue) {
|
||||
return {
|
||||
...prev,
|
||||
queue: [...prev.queue.slice(1), newValue] as SnackBarOptions[],
|
||||
queue: [...prev.queue, newValue] as SnackBarOptions[],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...prev,
|
||||
queue: [...prev.queue, newValue] as SnackBarOptions[],
|
||||
};
|
||||
}),
|
||||
[scopeId],
|
||||
},
|
||||
),
|
||||
[componentInstanceId],
|
||||
);
|
||||
|
||||
const enqueueSuccessSnackBar = useCallback(
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
|
||||
type SnackBarComponentInstanceContextProviderProps = {
|
||||
children: ReactNode;
|
||||
snackBarComponentInstanceId: string;
|
||||
};
|
||||
|
||||
export const SnackBarComponentInstanceContextProvider = ({
|
||||
children,
|
||||
snackBarComponentInstanceId,
|
||||
}: SnackBarComponentInstanceContextProviderProps) => {
|
||||
return (
|
||||
<SnackBarComponentInstanceContext.Provider
|
||||
value={{ instanceId: snackBarComponentInstanceId }}
|
||||
>
|
||||
{children}
|
||||
</SnackBarComponentInstanceContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { SnackBarManagerScopeInternalContext } from './scope-internal-context/SnackBarManagerScopeInternalContext';
|
||||
|
||||
type SnackBarProviderScopeProps = {
|
||||
children: ReactNode;
|
||||
snackBarManagerScopeId: string;
|
||||
};
|
||||
|
||||
export const SnackBarProviderScope = ({
|
||||
children,
|
||||
snackBarManagerScopeId,
|
||||
}: SnackBarProviderScopeProps) => {
|
||||
return (
|
||||
<SnackBarManagerScopeInternalContext.Provider
|
||||
value={{ scopeId: snackBarManagerScopeId }}
|
||||
>
|
||||
{children}
|
||||
</SnackBarManagerScopeInternalContext.Provider>
|
||||
);
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext';
|
||||
import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey';
|
||||
|
||||
type SnackBarManagerScopeInternalContextProps = RecoilComponentStateKey;
|
||||
|
||||
export const SnackBarManagerScopeInternalContext =
|
||||
createScopeInternalContext<SnackBarManagerScopeInternalContextProps>();
|
||||
@ -0,0 +1,22 @@
|
||||
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { SnackBarProps } from '../components/SnackBar';
|
||||
|
||||
export type SnackBarOptions = SnackBarProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SnackBarState = {
|
||||
maxQueue: number;
|
||||
queue: SnackBarOptions[];
|
||||
};
|
||||
|
||||
export const snackBarInternalComponentState =
|
||||
createComponentStateV2<SnackBarState>({
|
||||
key: 'snackBarState',
|
||||
defaultValue: {
|
||||
maxQueue: 3,
|
||||
queue: [],
|
||||
},
|
||||
componentInstanceContext: SnackBarComponentInstanceContext,
|
||||
});
|
||||
@ -1,20 +0,0 @@
|
||||
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
|
||||
|
||||
import { SnackBarProps } from '../components/SnackBar';
|
||||
|
||||
export type SnackBarOptions = SnackBarProps & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SnackBarState = {
|
||||
maxQueue: number;
|
||||
queue: SnackBarOptions[];
|
||||
};
|
||||
|
||||
export const snackBarInternalScopedState = createComponentState<SnackBarState>({
|
||||
key: 'snackBarState',
|
||||
defaultValue: {
|
||||
maxQueue: 3,
|
||||
queue: [],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user