diff --git a/core/hooks/__tests__/useBlockUser.test.tsx b/core/hooks/__tests__/useBlockUser.test.tsx new file mode 100644 index 0000000000000000000000000000000000000000..0841472f266c80a33e832c9089c7a0ae15ae9cc1 --- /dev/null +++ b/core/hooks/__tests__/useBlockUser.test.tsx @@ -0,0 +1,74 @@ +import { renderHook } from '@testing-library/react-hooks' +import { Provider, createStore } from 'jotai' +import type { MatrixClient } from 'matrix-js-sdk' +import React from 'react' +import type { PropsWithChildren } from 'react' + +import type ChatClient from '@holi/chat/src/client/ChatClient' +import { initChatClient } from '@holi/chat/src/store/store' +import { createChatClient, createMatrixClientMock } from '@holi/chat/src/test-utils' +import { initChatRoomFixture } from '@holi/chat/src/test-utils/matrix-fixtures' +import { useBlockUser } from '@holi/core/hooks/useBlockUser' +import { BlockUserMutation } from '@holi/core/queries' +import { MockedProvider } from '@apollo/client/testing' +import type { User } from '@holi/core/domain/shared/types' +import { waitFor } from '@testing-library/react-native' +import { toMatrixIdentity } from '@holi/chat/src/store' + +jest.mock('matrix-js-sdk') +jest.mock('@holi/chat/src/client/pusher') + +const blockUserRequest = { + request: { + query: BlockUserMutation, + variables: { userId: 'holi-uuid' }, + }, + result: { data: { success: true } }, +} + +describe('useBlockUser', () => { + const ApolloJotaiProvider = ({ children }: PropsWithChildren) => ( + <MockedProvider mocks={[blockUserRequest]}> + <Provider store={store}>{children}</Provider> + </MockedProvider> + ) + let store: ReturnType<typeof createStore> + let mxClient: jest.MockedObjectDeep<MatrixClient> + let chatClient: ChatClient + + beforeEach(() => { + jest.clearAllTimers() + mxClient = createMatrixClientMock() + chatClient = createChatClient() + initChatClient(chatClient) + store = createStore() + }) + + it('should call Matrix client `setIgnoredUsers()` method with Matrix ID for blocked user.', async () => { + const { member } = initChatRoomFixture(mxClient) + + const { result } = renderHook(() => useBlockUser(), { wrapper: ApolloJotaiProvider }) + const blockUser = result.current[1] + + await waitFor(() => blockUser({ id: 'holi-uuid', identity: member.id } as User)) + + expect(mxClient.setIgnoredUsers).toHaveBeenCalledWith([toMatrixIdentity(member.id)]) + }) + + it('shoud call `onHoliUserBlocked` and `onChatUserBlocked` when passsed.', async () => { + const { member } = initChatRoomFixture(mxClient) + + const { result } = renderHook(() => useBlockUser(), { wrapper: ApolloJotaiProvider }) + const blockUser = result.current[1] + + const onHoliUserBlocked = jest.fn() + const onChatUserBlocked = jest.fn() + + await waitFor(() => + blockUser({ id: 'holi-uuid', identity: member.id } as User, { onHoliUserBlocked, onChatUserBlocked }) + ) + + expect(onHoliUserBlocked).toHaveBeenCalled() + expect(onChatUserBlocked).toHaveBeenCalled() + }) +}) diff --git a/core/hooks/useBlockUser.ts b/core/hooks/useBlockUser.ts index c35c864450dbd5d7d7d72152dd81443051d8b951..929b1c87b08080ee3fdaf28d930d6f9cb76bac00 100644 --- a/core/hooks/useBlockUser.ts +++ b/core/hooks/useBlockUser.ts @@ -1,30 +1,57 @@ import { useMutation } from '@apollo/client' -import type { FetchResult, MutationFunctionOptions } from '@apollo/client' +import { getExistingRoomId, ignoreUser, toMatrixIdentity, useSetRemoveRoomAtom } from '@holi/chat/src/store' +import type { User } from '@holi/core/domain/shared/types' import { useErrorHandling } from '@holi/core/errors/hooks' import { BlockUserMutation } from '@holi/core/queries' import type { BlockUserResponse } from '@holi/core/queries' import { HoliToastType, useToast } from '@holi/ui/components/molecules/HoliToastProvider' +import { useState } from 'react' -export type UseBlockUser = [boolean, (options?: MutationFunctionOptions) => Promise<FetchResult>] +interface BlockUserOptions { + successMsg?: string + onHoliUserBlocked?: () => void + onChatUserBlocked?: () => void +} -const useBlockUser = (userId: string, successMsg: string): UseBlockUser => { +export const useBlockUser = () => { const { displayError } = useErrorHandling() const { openToast } = useToast() - const [blockUser, { loading: isLoadingBlockUser }] = useMutation<BlockUserResponse>(BlockUserMutation, { - variables: { userId }, - onCompleted: () => { - openToast(successMsg, HoliToastType.SUCCESS) - }, - onError: (e) => { - displayError(e, 'Failed to block user', { - location: 'useBlockUser', - }) - }, - }) + const [blockHoliUser] = useMutation<BlockUserResponse>(BlockUserMutation) - return [isLoadingBlockUser, blockUser] -} + const removeRoom = useSetRemoveRoomAtom() + const removeChatsWithBlockedUser = (chatUserId: string) => { + const roomId = getExistingRoomId([chatUserId], 'private') + if (roomId) removeRoom(roomId) + } + + const [isLoading, setIsLoading] = useState(false) -export default useBlockUser + return [ + isLoading, + async ( + user: User, + { successMsg, onHoliUserBlocked = () => null, onChatUserBlocked = () => null }: BlockUserOptions = {} + ) => { + setIsLoading(true) + + const chatUserId = toMatrixIdentity(user.identity) + try { + await Promise.all([ + blockHoliUser({ variables: { userId: user.id } }).then(onHoliUserBlocked), + ignoreUser(chatUserId).then(onChatUserBlocked), + ]) + removeChatsWithBlockedUser(chatUserId) + + if (successMsg) openToast(successMsg, HoliToastType.SUCCESS) + } catch (e) { + displayError(e, 'Failed to block user', { + location: 'useBlockUser', + }) + } finally { + setIsLoading(false) + } + }, + ] as const +} diff --git a/core/screens/chat/ChatRoomView/PrivateRoomView.tsx b/core/screens/chat/ChatRoomView/PrivateRoomView.tsx index 16432a2144eff5764ee64e1120a7f235aa3cf98d..469d13010032f82523a86196657c4e294b5b64b0 100644 --- a/core/screens/chat/ChatRoomView/PrivateRoomView.tsx +++ b/core/screens/chat/ChatRoomView/PrivateRoomView.tsx @@ -42,7 +42,7 @@ const PrivateRoomView = ({ const chatPartner = chatPartners?.[0] const chatPartnerHoliUser = chatPartnerHoliUsers[0] - return ( + return chatPartnerHoliUser ? ( <ChatRoomHeaderTitle href={'/profile/' + chatPartnerHoliUser.id} name={room.name} @@ -51,7 +51,7 @@ const PrivateRoomView = ({ initials={getChatInitials(chatPartner?.name)} defaultColor={getDefaultAvatarColor(chatPartner?.name)} /> - ) + ) : null } return ( diff --git a/core/screens/userprofile/UserProfile.tsx b/core/screens/userprofile/UserProfile.tsx index 2d5922c46f4be93af2c6f8da14726ec863a185e9..a85c51d9e5a2bfbe36de17c874608551d2117349 100644 --- a/core/screens/userprofile/UserProfile.tsx +++ b/core/screens/userprofile/UserProfile.tsx @@ -14,7 +14,7 @@ import { isNotFoundError } from '@holi/core/errors/helpers' import { useErrorHandling } from '@holi/core/errors/hooks' import { isSSR } from '@holi/core/helpers/isSSR' import useIsomorphicLayoutEffect from '@holi/core/helpers/useIsomorphicLayoutEffect' -import useBlockUser from '@holi/core/hooks/useBlockUser' +import { useBlockUser } from '@holi/core/hooks/useBlockUser' import useAuthRequiredNavigation from '@holi/core/navigation/hooks/useAuthRequiredNavigation' import createParamHooks from '@holi/core/navigation/hooks/useParam' import useRouting from '@holi/core/navigation/hooks/useRouting' @@ -87,9 +87,8 @@ const UserProfile = () => { const user = data?.userById const userFullName = user?.fullName || t('user.profileView.anonymous') - const successMsg = t('user.profileView.block.successMessage', { name: userFullName }) - const [isLoadingBlockUser, blockUser] = useBlockUser(userId, successMsg) + const [isBlockingUser, blockUser] = useBlockUser() useIsomorphicLayoutEffect(() => { setScreenOptions({ @@ -103,14 +102,17 @@ const UserProfile = () => { /> <HoliActionDrawer.Action icon={CloseCircle} - isLoading={isLoadingBlockUser} + isLoading={isBlockingUser} title={t('profile.block', { name: userFullName })} showConfirmation > <HoliActionDrawer.Confirmation title={t('user.profileView.block.confirmation.title', { name: userFullName })} description={t('user.profileView.block.confirmation.description', { name: userFullName })} - handleConfirmation={blockUser} + handleConfirmation={() => { + if (user) + blockUser(user, { successMsg: t('user.profileView.block.successMessage', { name: userFullName }) }) + }} hasDismissBtn requireLogin /> @@ -119,7 +121,7 @@ const UserProfile = () => { ), headerLeft: ({ tintColor }) => <HeaderBackButton tintColor={tintColor} fallbackRoute="/" />, }) - }, [isLoadingBlockUser, t, userFullName, blockUser, navigateWithAuth, userId, setScreenOptions]) + }, [t, userFullName, blockUser, navigateWithAuth, userId, setScreenOptions, user, isBlockingUser]) if (!userId || (loading && !user)) { return ( diff --git a/core/screens/userprofile/components/StartChatButton.tsx b/core/screens/userprofile/components/StartChatButton.tsx index a9cabeb1b0a2c98ec43258bdb9b29f891db5e1ad..54e6916d3ae58a30ea046ff4e4fb43e4840c1f97 100644 --- a/core/screens/userprofile/components/StartChatButton.tsx +++ b/core/screens/userprofile/components/StartChatButton.tsx @@ -71,6 +71,7 @@ export function StartChatButton({ label, doReplaceRoute, user: chatPartnerUser, label={label} onPress={handleChat} testID="user-profile-open-chat" + loading={!isChatInitialized} {...rest} /> ) diff --git a/packages/chat/src/components/ChatRoomActionDrawer.tsx b/packages/chat/src/components/ChatRoomActionDrawer.tsx index 75ee8c413ba2da51cbad9e80c180c977960153e7..f133f67cb2c5956974c3cc42f499f4301047c34b 100644 --- a/packages/chat/src/components/ChatRoomActionDrawer.tsx +++ b/packages/chat/src/components/ChatRoomActionDrawer.tsx @@ -1,13 +1,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { ignoreUser, leaveRoom, useSetRemoveRoomAtom } from '@holi/chat/src/store' +import { leaveRoom } from '@holi/chat/src/store' import useRouting from '@holi/core/navigation/hooks/useRouting' import { CloseCircle, Delete } from '@holi/icons/src/generated' import { HoliToastType, useToast } from '@holi/ui/components/molecules/HoliToastProvider' import HoliActionDrawer from '@holi/ui/components/organisms/HoliActionDrawer' import { useTheme } from '@holi/ui/styles/theme' import type { User } from '@holi/core/domain/shared/types' +import { useBlockUser } from '@holi/core/hooks/useBlockUser' interface Props { chatPartnerFullName: string @@ -19,15 +20,15 @@ const ChatRoomActionDrawer = ({ chatPartnerFullName, roomId, usersToBlock }: Pro const { t } = useTranslation() const { navigate } = useRouting() const { openToast } = useToast() - const removeRoom = useSetRemoveRoomAtom() const { theme } = useTheme() const { colors } = theme + const [isBlockingUser, blockUser] = useBlockUser() const handleBlockConfirmation = async (user: User) => { - await ignoreUser(user.id) - navigate('/chat') - removeRoom(roomId) - openToast(t('user.profileView.block.successMessage', { name: user.fullName }), HoliToastType.SUCCESS) + blockUser(user, { + successMsg: t('user.profileView.block.successMessage', { name: user.fullName }), + onChatUserBlocked: () => navigate('/chat'), + }) } const handleLeaveConfirmation = async () => { @@ -45,6 +46,7 @@ const ChatRoomActionDrawer = ({ chatPartnerFullName, roomId, usersToBlock }: Pro iconColor={colors.error20} variant="danger" showConfirmation + isLoading={isBlockingUser} key={user.fullName} > <HoliActionDrawer.Confirmation diff --git a/packages/chat/src/store/store.ts b/packages/chat/src/store/store.ts index 186ba313ad856f3bf86cd01341f4d6555cdcf157..a82d7625d345cec1386eedbb811330fef3e0bb38 100644 --- a/packages/chat/src/store/store.ts +++ b/packages/chat/src/store/store.ts @@ -736,7 +736,7 @@ _membershipSubscriberAtom.onMount = (setAtom) => { const _ignoredUsersAtom = atom<string[]>([]) const _ignoredUsersAtomSubscriber = atom(null, async (_get, set) => { - set(_ignoredUsersAtom, getChatClient().getIgnoredUsers) + set(_ignoredUsersAtom, () => getChatClient().getIgnoredUsers()) }) _ignoredUsersAtomSubscriber.onMount = (setAtom) => {