From 7e3d44c24ad6013b473ca36706d26a1bdef7ac07 Mon Sep 17 00:00:00 2001 From: Marcus Pasell <3690498+rickyrombo@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:41:50 -0700 Subject: [PATCH] [PAY-924][Desktop] DMs: Blocking/Unblocking Wire Up/UI (#3155) --- packages/common/src/store/pages/chat/sagas.ts | 68 ++++++++++++++++++- .../common/src/store/pages/chat/selectors.ts | 2 + packages/common/src/store/pages/chat/slice.ts | 19 +++++- .../web/src/common/store/account/sagas.js | 7 +- .../src/components/stat-banner/StatBanner.tsx | 29 +++++--- .../components/CreateChatUserResult.tsx | 34 +++++++--- .../profile-page/ProfilePageProvider.tsx | 38 +++++++++-- .../components/desktop/ProfilePage.tsx | 9 +++ 8 files changed, 178 insertions(+), 28 deletions(-) diff --git a/packages/common/src/store/pages/chat/sagas.ts b/packages/common/src/store/pages/chat/sagas.ts index d443b677d48..56edf0efe2f 100644 --- a/packages/common/src/store/pages/chat/sagas.ts +++ b/packages/common/src/store/pages/chat/sagas.ts @@ -7,7 +7,7 @@ import { Status } from 'models/Status' import { getAccountUser, getUserId } from 'store/account/selectors' import { setVisibility } from 'store/ui/modals/slice' -import { decodeHashId, encodeHashId } from '../../../utils' +import { decodeHashId, encodeHashId, removeNullable } from '../../../utils' import { cacheUsersActions } from '../../cache' import { getContext } from '../../effects' @@ -33,7 +33,11 @@ const { markChatAsReadFailed, sendMessage, sendMessageFailed, - addMessage + addMessage, + fetchBlockees, + fetchBlockeesSucceeded, + unblockUser, + blockUser } = chatActions const { getChatsSummary, getChat } = chatSelectors @@ -257,6 +261,49 @@ function* fetchChatIfNecessary(args: { chatId: string }) { } } +function* doFetchBlockees() { + try { + const audiusSdk = yield* getContext('audiusSdk') + const sdk = yield* call(audiusSdk) + const { data } = yield* call([sdk.chats, sdk.chats.getBlockees]) + yield* put( + fetchBlockeesSucceeded({ + blockees: data + .map((encodedId) => decodeHashId(encodedId)) + .filter(removeNullable) + }) + ) + } catch (e) { + console.error('fetchBlockeesFailed', e) + } +} + +function* doBlockUser(action: ReturnType) { + try { + const audiusSdk = yield* getContext('audiusSdk') + const sdk = yield* call(audiusSdk) + yield* call([sdk.chats, sdk.chats.block], { + userId: encodeHashId(action.payload.userId) + }) + yield* put(fetchBlockees()) + } catch (e) { + console.error('blockUserFailed', e) + } +} + +function* doUnblockUser(action: ReturnType) { + try { + const audiusSdk = yield* getContext('audiusSdk') + const sdk = yield* call(audiusSdk) + yield* call([sdk.chats, sdk.chats.unblock], { + userId: encodeHashId(action.payload.userId) + }) + yield* put(fetchBlockees()) + } catch (e) { + console.error('unblockUserFailed', e) + } +} + function* watchAddMessage() { yield takeEvery(addMessage, ({ payload }) => fetchChatIfNecessary(payload)) } @@ -285,6 +332,18 @@ function* watchMarkChatAsRead() { yield takeEvery(markChatAsRead, doMarkChatAsRead) } +function* watchFetchBlockees() { + yield takeLatest(fetchBlockees, doFetchBlockees) +} + +function* watchBlockUser() { + yield takeEvery(blockUser, doBlockUser) +} + +function* watchUnblockUser() { + yield takeEvery(unblockUser, doUnblockUser) +} + export const sagas = () => { return [ watchFetchChats, @@ -293,6 +352,9 @@ export const sagas = () => { watchCreateChat, watchMarkChatAsRead, watchSendMessage, - watchAddMessage + watchAddMessage, + watchFetchBlockees, + watchBlockUser, + watchUnblockUser ] } diff --git a/packages/common/src/store/pages/chat/selectors.ts b/packages/common/src/store/pages/chat/selectors.ts index 537dc546224..e9d141f987a 100644 --- a/packages/common/src/store/pages/chat/selectors.ts +++ b/packages/common/src/store/pages/chat/selectors.ts @@ -30,6 +30,8 @@ export const getOptimisticReads = (state: CommonState) => export const getOptimisticReactions = (state: CommonState) => state.pages.chat.optimisticReactions +export const getBlockees = (state: CommonState) => state.pages.chat.blockees + export const getChats = createSelector( [selectAllChats, getOptimisticReads], (chats, optimisticReads) => { diff --git a/packages/common/src/store/pages/chat/slice.ts b/packages/common/src/store/pages/chat/slice.ts index 3b1cbebf0af..f6f92317bcf 100644 --- a/packages/common/src/store/pages/chat/slice.ts +++ b/packages/common/src/store/pages/chat/slice.ts @@ -41,6 +41,7 @@ type ChatState = { optimisticReactions: Record optimisticChatRead: Record activeChatId: string | null + blockees: ID[] } type SetMessageReactionPayload = { @@ -81,7 +82,8 @@ const initialState: ChatState = { messages: {}, optimisticChatRead: {}, optimisticReactions: {}, - activeChatId: null + activeChatId: null, + blockees: [] } const slice = createSlice({ @@ -340,6 +342,21 @@ const slice = createSlice({ }, disconnect: (_state, _action: Action) => { // triggers middleware + }, + fetchBlockees: (_state, _action: Action) => { + // triggers saga + }, + fetchBlockeesSucceeded: ( + state, + action: PayloadAction<{ blockees: ID[] }> + ) => { + state.blockees = action.payload.blockees + }, + blockUser: (_state, _action: PayloadAction<{ userId: ID }>) => { + // triggers saga + }, + unblockUser: (_state, _action: PayloadAction<{ userId: ID }>) => { + // triggers saga } } }) diff --git a/packages/web/src/common/store/account/sagas.js b/packages/web/src/common/store/account/sagas.js index 5f020e45169..810ca7eb689 100644 --- a/packages/web/src/common/store/account/sagas.js +++ b/packages/web/src/common/store/account/sagas.js @@ -8,7 +8,8 @@ import { solanaSelectors, createUserBankIfNeeded, getContext, - FeatureFlags + FeatureFlags, + chatActions } from '@audius/common' import { call, put, fork, select, takeEvery } from 'redux-saga/effects' @@ -22,6 +23,7 @@ import disconnectedWallets from './disconnected_wallet_fix.json' const { fetchProfile } = profilePageActions const { getFeePayer } = solanaSelectors +const { fetchBlockees } = chatActions const { getUserId, @@ -256,6 +258,9 @@ function* cacheAccount(account) { yield call([localStorage, 'setAudiusAccountUser'], account) yield put(fetchAccountSucceeded(formattedAccount)) + + // Fetch user's chat blockee list after fetching their account + yield put(fetchBlockees()) } // Pull from redux cache and persist to local storage cache diff --git a/packages/web/src/components/stat-banner/StatBanner.tsx b/packages/web/src/components/stat-banner/StatBanner.tsx index d8207b21efd..d5b7a9eb810 100644 --- a/packages/web/src/components/stat-banner/StatBanner.tsx +++ b/packages/web/src/components/stat-banner/StatBanner.tsx @@ -10,7 +10,8 @@ import { IconKebabHorizontal, IconMessage, PopupMenu, - IconUnblockMessages + IconUnblockMessages, + IconBlockMessages } from '@audius/stems' import cn from 'classnames' @@ -58,6 +59,9 @@ type StatsBannerProps = { isSubscribed?: boolean onToggleSubscribe?: () => void onMessage?: () => void + onBlock?: () => void + onUnblock?: () => void + isBlocked?: boolean } export const StatBanner = (props: StatsBannerProps) => { @@ -78,8 +82,11 @@ export const StatBanner = (props: StatsBannerProps) => { onCancel, onFollow, onUnfollow, - onMessage, following, + onMessage, + onBlock, + onUnblock, + isBlocked, isSubscribed, onToggleSubscribe } = props @@ -146,13 +153,17 @@ export const StatBanner = (props: StatsBannerProps) => { onClick: onShare!, icon: }, - { - text: messages.unblockMessages, - onClick: () => { - // TODO - }, - icon: - } + isBlocked + ? { + text: messages.unblockMessages, + onClick: onUnblock!, + icon: + } + : { + text: messages.blockMessages, + onClick: onBlock!, + icon: + } ]} transformOrigin={{ horizontal: 'right', vertical: 'top' }} anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} diff --git a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx index 4edc538c378..c3beba0a388 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx @@ -3,6 +3,7 @@ import { useCallback, useEffect } from 'react' import { accountSelectors, chatActions, + chatSelectors, tippingActions, tippingSelectors, User @@ -12,6 +13,7 @@ import { IconButton, IconKebabHorizontal, IconMessage, + IconUnblockMessages, IconUser, PopupMenu, PopupPosition @@ -29,7 +31,8 @@ const messages = { moreOptions: 'More options', message: 'Message This User', visit: "Visit User's Profile", - block: 'Block Messages' + block: 'Block Messages', + unblock: 'Unblock Messages' } type UserResultComposeProps = { @@ -41,7 +44,8 @@ const { getUserId } = accountSelectors const { getOptimisticSupporters, getOptimisticSupporting } = tippingSelectors const { fetchSupportersForUser } = tippingActions -const { createChat } = chatActions +const { createChat, blockUser, unblockUser } = chatActions +const { getBlockees } = chatSelectors const renderTrigger = ( anchorRef: React.MutableRefObject, @@ -61,6 +65,8 @@ export const MessageUserSearchResult = (props: UserResultComposeProps) => { const currentUserId = useSelector(getUserId) const supportingMap = useSelector(getOptimisticSupporting) const supportersMap = useSelector(getOptimisticSupporters) + const blockeeList = useSelector(getBlockees) + const isBlocked = blockeeList.includes(user.user_id) const handleComposeClicked = useCallback(() => { dispatch(createChat({ userIds: [user.user_id] })) @@ -72,8 +78,12 @@ export const MessageUserSearchResult = (props: UserResultComposeProps) => { }, [dispatch, user, closeModal]) const handleBlockClicked = useCallback(() => { - // TODO - }, []) + dispatch(blockUser({ userId: user.user_id })) + }, [dispatch, user]) + + const handleUnblockClicked = useCallback(() => { + dispatch(unblockUser({ userId: user.user_id })) + }, [dispatch, user]) const items = [ { @@ -82,11 +92,17 @@ export const MessageUserSearchResult = (props: UserResultComposeProps) => { onClick: handleComposeClicked }, { icon: , text: messages.visit, onClick: handleVisitClicked }, - { - icon: , - text: messages.block, - onClick: handleBlockClicked - } + isBlocked + ? { + icon: , + text: messages.unblock, + onClick: handleUnblockClicked + } + : { + icon: , + text: messages.block, + onClick: handleBlockClicked + } ] useEffect(() => { diff --git a/packages/web/src/pages/profile-page/ProfilePageProvider.tsx b/packages/web/src/pages/profile-page/ProfilePageProvider.tsx index 65829c4c8fe..f8d06a6b1a9 100644 --- a/packages/web/src/pages/profile-page/ProfilePageProvider.tsx +++ b/packages/web/src/pages/profile-page/ProfilePageProvider.tsx @@ -35,7 +35,8 @@ import { playerSelectors, queueSelectors, Nullable, - chatActions + chatActions, + chatSelectors } from '@audius/common' import { push as pushRoute, replace } from 'connected-react-router' import { UnregisterCallback } from 'history' @@ -73,7 +74,8 @@ const { getProfileUserId } = profilePageSelectors const { getAccountUser } = accountSelectors -const { createChat } = chatActions +const { createChat, blockUser, unblockUser } = chatActions +const { getBlockees } = chatSelectors const INITIAL_UPDATE_FIELDS = { updatedName: null, @@ -693,6 +695,20 @@ class ProfilePage extends PureComponent { return this.props.onMessage(profile!.user_id) } + onBlock = () => { + const { + profile: { profile } + } = this.props + return this.props.onBlock(profile!.user_id) + } + + onUnblock = () => { + const { + profile: { profile } + } = this.props + return this.props.onUnblock(profile!.user_id) + } + render() { const { profile: { @@ -883,7 +899,9 @@ class ProfilePage extends PureComponent { updateDonation: this.updateDonation, updateCoverPhoto: this.updateCoverPhoto, didChangeTabsFrom: this.didChangeTabsFrom, - onMessage: this.onMessage + onMessage: this.onMessage, + onBlock: this.onBlock, + onUnblock: this.onUnblock } const mobileProps = { @@ -924,7 +942,10 @@ class ProfilePage extends PureComponent { openCreatePlaylistModal, - updateProfile: this.props.updateProfile + updateProfile: this.props.updateProfile, + isBlocked: this.props.profile.profile + ? this.props.blockedList.includes(this.props.profile.profile.user_id) + : false } return ( @@ -962,7 +983,8 @@ function makeMapStateToProps() { }), relatedArtists: getRelatedArtists(state, { id: getProfileUserId(state, handleLower) ?? 0 - }) + }), + blockedList: getBlockees(state) } } return mapStateToProps @@ -1122,6 +1144,12 @@ function mapDispatchToProps(dispatch: Dispatch, props: RouteComponentProps) { }, onMessage: (userId: ID) => { dispatch(createChat({ userIds: [userId] })) + }, + onBlock: (userId: ID) => { + dispatch(blockUser({ userId })) + }, + onUnblock: (userId: ID) => { + dispatch(unblockUser({ userId })) } } } diff --git a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx index 87d49341f17..764dc4b6856 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx @@ -89,6 +89,7 @@ export type ProfilePageProps = { isSubscribed: boolean mode: ProfileMode stats: StatProps[] + isBlocked: boolean profile: ProfileUser | null albums: Collection[] | null @@ -147,6 +148,8 @@ export type ProfilePageProps = { didChangeTabsFrom: (prevLabel: string, currentLabel: string) => void onCloseArtistRecommendations: () => void onMessage: () => void + onBlock: () => void + onUnblock: () => void } const ProfilePage = ({ @@ -201,6 +204,9 @@ const ProfilePage = ({ areArtistRecommendationsVisible, onCloseArtistRecommendations, onMessage, + onBlock, + onUnblock, + isBlocked, accountUserId, userId, @@ -739,6 +745,9 @@ const ProfilePage = ({ onFollow={onFollow} onUnfollow={onUnfollow} onMessage={onMessage} + isBlocked={isBlocked} + onBlock={onBlock} + onUnblock={onUnblock} />