diff --git a/packages/common/src/store/reducers.ts b/packages/common/src/store/reducers.ts index d937564fec..0304c5666c 100644 --- a/packages/common/src/store/reducers.ts +++ b/packages/common/src/store/reducers.ts @@ -97,6 +97,10 @@ import deletePlaylistConfirmationReducer from './ui/delete-playlist-confirmation import { DeletePlaylistConfirmationModalState } from './ui/delete-playlist-confirmation-modal/types' import duplicateAddConfirmationReducer from './ui/duplicate-add-confirmation-modal/slice' import { DuplicateAddConfirmationModalState } from './ui/duplicate-add-confirmation-modal/types' +import { + InboxUnavailableModalState, + inboxUnavailableModalReducer +} from './ui/inbox-unavailable-modal' import { LeavingAudiusModalState, leavingAudiusModalReducer @@ -200,6 +204,8 @@ export const reducers = () => ({ mobileOverflowModal: mobileOverflowModalReducer, modals: modalsReducer, modalsWithState: combineReducers({ + createChatModal: createChatModalReducer, + inboxUnavailableModal: inboxUnavailableModalReducer, leavingAudiusModal: leavingAudiusModalReducer }), musicConfetti: musicConfettiReducer, @@ -318,7 +324,6 @@ export type CommonState = { buyAudio: ReturnType addToPlaylist: AddToPlaylistState changePassword: ChangePasswordState - createChatModal: CreateChatModalState createPlaylistModal: CreatePlaylistModalState collectibleDetails: CollectibleDetailsState deletePlaylistConfirmationModal: DeletePlaylistConfirmationModalState @@ -327,6 +332,8 @@ export type CommonState = { mobileOverflowModal: MobileOverflowModalState modals: ModalsState modalsWithState: { + createChatModal: CreateChatModalState + inboxUnavailableModal: InboxUnavailableModalState leavingAudiusModal: LeavingAudiusModalState } musicConfetti: MusicConfettiState diff --git a/packages/common/src/store/tipping/slice.ts b/packages/common/src/store/tipping/slice.ts index 772dac9282..7c2d1e12f7 100644 --- a/packages/common/src/store/tipping/slice.ts +++ b/packages/common/src/store/tipping/slice.ts @@ -1,4 +1,4 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit' import { TipSource } from '../../models/Analytics' import { ID } from '../../models/Identifiers' @@ -104,6 +104,8 @@ const slice = createSlice({ user: User | null source: TipSource trackId?: ID + onSuccessActions?: Action[] + onSuccessConfirmedActions?: Action[] }> ) => { if (!action.payload.user) { @@ -113,6 +115,9 @@ const slice = createSlice({ state.send.source = action.payload.source state.send.user = action.payload.user state.send.trackId = action.payload.trackId ?? null + state.send.onSuccessActions = action.payload.onSuccessActions + state.send.onSuccessConfirmedActions = + action.payload.onSuccessConfirmedActions }, sendTip: (state, action: PayloadAction<{ amount: string }>) => { if (state.send.status !== 'SEND') { diff --git a/packages/common/src/store/tipping/types.ts b/packages/common/src/store/tipping/types.ts index 809220cb46..68d9f061a8 100644 --- a/packages/common/src/store/tipping/types.ts +++ b/packages/common/src/store/tipping/types.ts @@ -1,3 +1,5 @@ +import type { Action } from '@reduxjs/toolkit' + import { TipSource } from '../../models/Analytics' import { ID } from '../../models/Identifiers' import { Supporter, Supporting, UserTip } from '../../models/Tipping' @@ -49,6 +51,14 @@ export type TippingState = { error: Nullable source: TipSource trackId: Nullable // in case the user is sending a tip from a gated track page / modal + /** + * Actions to fire when the tip sends successfully + */ + onSuccessActions?: Action[] + /** + * Actions to fire when the tip is confirmed to be indexed by discovery + */ + onSuccessConfirmedActions?: Action[] } tipToDisplay: Nullable showTip: boolean diff --git a/packages/common/src/store/ui/create-chat-modal/index.ts b/packages/common/src/store/ui/create-chat-modal/index.ts new file mode 100644 index 0000000000..7393586a4f --- /dev/null +++ b/packages/common/src/store/ui/create-chat-modal/index.ts @@ -0,0 +1,22 @@ +import { Action } from '@reduxjs/toolkit' + +import { createModal } from '../modals/createModal' + +export type CreateChatModalState = { + presetMessage?: string + onCancelAction?: Action +} + +const createChatModal = createModal({ + reducerPath: 'createChatModal', + initialState: { + isOpen: false + }, + sliceSelector: (state) => state.ui.modalsWithState +}) + +export const { + hook: useCreateChatModal, + actions: createChatModalActions, + reducer: createChatModalReducer +} = createChatModal diff --git a/packages/common/src/store/ui/create-chat-modal/selectors.ts b/packages/common/src/store/ui/create-chat-modal/selectors.ts deleted file mode 100644 index 1470e13e48..0000000000 --- a/packages/common/src/store/ui/create-chat-modal/selectors.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CommonState } from 'store/index' - -export const getPresetMessage = (state: CommonState) => - state.ui.createChatModal.presetMessage - -export const getOnCancelAction = (state: CommonState) => - state.ui.createChatModal.onCancelAction diff --git a/packages/common/src/store/ui/create-chat-modal/slice.ts b/packages/common/src/store/ui/create-chat-modal/slice.ts deleted file mode 100644 index 88bf23c9d2..0000000000 --- a/packages/common/src/store/ui/create-chat-modal/slice.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Action, createSlice, PayloadAction } from '@reduxjs/toolkit' - -export type CreateChatModalState = { - presetMessage?: string - onCancelAction?: Action -} - -const initialState: CreateChatModalState = {} - -// This is only really relevant on Web, where we don't have the ability to route the URL to the modal -// On mobile, the ChatUserListScreen is a screen and we can pass the presetMessage in as a param, -// and navigation is handled nicely, no need for a cancel action. -const slice = createSlice({ - name: 'application/ui/createChatModal', - initialState, - reducers: { - setState: ( - state, - action: PayloadAction<{ presetMessage?: string; onCancelAction?: Action }> - ) => { - state.presetMessage = action.payload.presetMessage - state.onCancelAction = action.payload.onCancelAction - } - } -}) - -export const actions = slice.actions - -export default slice.reducer diff --git a/packages/common/src/store/ui/inbox-unavailable-modal/index.ts b/packages/common/src/store/ui/inbox-unavailable-modal/index.ts new file mode 100644 index 0000000000..5411ee87e2 --- /dev/null +++ b/packages/common/src/store/ui/inbox-unavailable-modal/index.ts @@ -0,0 +1,26 @@ +import { Action } from '@reduxjs/toolkit' + +import { ID } from 'models/Identifiers' + +import { createModal } from '../modals/createModal' + +export type InboxUnavailableModalState = { + userId?: ID + presetMessage?: string + onCancelAction?: Action + onSuccessAction?: Action +} + +const inboxUnavailableModal = createModal({ + initialState: { + isOpen: false + }, + reducerPath: 'inboxUnavailableModal', + sliceSelector: (state) => state.ui.modalsWithState +}) + +export const { + hook: useInboxUnavailableModal, + actions: inboxUnavailableModalActions, + reducer: inboxUnavailableModalReducer +} = inboxUnavailableModal diff --git a/packages/common/src/store/ui/index.ts b/packages/common/src/store/ui/index.ts index 218d627957..4f15419ae4 100644 --- a/packages/common/src/store/ui/index.ts +++ b/packages/common/src/store/ui/index.ts @@ -140,15 +140,6 @@ export { export * as searchUsersModalSelectors from './search-users-modal/selectors' export { default as searchUsersModalSagas } from './search-users-modal/sagas' -export { - default as createChatModalReducer, - actions as createChatModalActions, - CreateChatModalState -} from './create-chat-modal/slice' -export * as createChatModalSelectors from './create-chat-modal/selectors' - -export { - leavingAudiusModalReducer, - useLeavingAudiusModal, - LeavingAudiusModalState -} from './leaving-audius-modal' +export * from './create-chat-modal' +export * from './leaving-audius-modal' +export * from './inbox-unavailable-modal' diff --git a/packages/common/src/store/ui/modals/createModal.ts b/packages/common/src/store/ui/modals/createModal.ts index 8d11aa3d66..2e8319497c 100644 --- a/packages/common/src/store/ui/modals/createModal.ts +++ b/packages/common/src/store/ui/modals/createModal.ts @@ -27,8 +27,8 @@ export const createModal = ({ name: `modals/${reducerPath}`, initialState, reducers: { - open: (_, action: PayloadAction) => { - return action.payload + open: (_, action: PayloadAction) => { + return { ...action.payload, isOpen: true } }, close: (state) => { state.isOpen = 'closing' @@ -99,6 +99,7 @@ export const createModal = ({ return { hook: useModal, + actions: slice.actions, reducer: slice.reducer } } diff --git a/packages/common/src/store/ui/modals/slice.ts b/packages/common/src/store/ui/modals/slice.ts index 7585886654..330ffed30c 100644 --- a/packages/common/src/store/ui/modals/slice.ts +++ b/packages/common/src/store/ui/modals/slice.ts @@ -33,7 +33,6 @@ const initialState: ModalsState = { TransactionDetails: false, VipDiscord: false, StripeOnRamp: false, - CreateChat: false, InboxSettings: false, LockedContent: false, PlaybackRate: false, diff --git a/packages/common/src/store/ui/modals/types.ts b/packages/common/src/store/ui/modals/types.ts index 4a2a4e53b7..e93ed80723 100644 --- a/packages/common/src/store/ui/modals/types.ts +++ b/packages/common/src/store/ui/modals/types.ts @@ -28,7 +28,6 @@ export type Modals = | 'TransactionDetails' | 'VipDiscord' | 'StripeOnRamp' - | 'CreateChat' | 'InboxSettings' | 'LockedContent' | 'PlaybackRate' diff --git a/packages/common/src/store/ui/search-users-modal/selectors.ts b/packages/common/src/store/ui/search-users-modal/selectors.ts index 65463ef413..59f4d2546d 100644 --- a/packages/common/src/store/ui/search-users-modal/selectors.ts +++ b/packages/common/src/store/ui/search-users-modal/selectors.ts @@ -2,3 +2,6 @@ import { CommonState } from 'store/reducers' export const getUserList = (state: CommonState) => state.ui.searchUsersModal.userList + +export const getLastSearchQuery = (state: CommonState) => + state.ui.searchUsersModal.lastQuery diff --git a/packages/web/src/common/store/tipping/sagas.ts b/packages/web/src/common/store/tipping/sagas.ts index 47c9ba6c1d..20a8ad1325 100644 --- a/packages/web/src/common/store/tipping/sagas.ts +++ b/packages/web/src/common/store/tipping/sagas.ts @@ -272,7 +272,13 @@ function* sendTipAsync() { } const sendTipData = yield* select(getSendTipData) - const { user: recipient, amount, source, trackId } = sendTipData + const { + user: recipient, + amount, + source, + trackId, + onSuccessConfirmedActions + } = sendTipData if (!recipient) { return } @@ -385,15 +391,14 @@ function* sendTipAsync() { yield* put( fetchPermissions({ userIds: [sender.user_id, recipient.user_id] }) ) + if (onSuccessConfirmedActions) { + // Spread here to unfreeze the action + // Redux sagas can't "put" frozen actions + for (const action of onSuccessConfirmedActions) { + yield* put({ ...action }) + } + } if (source === 'inboxUnavailableModal') { - console.debug('Creating chat silently...') - // Create the chat but don't navigate - yield* put( - chatActions.createChat({ - userIds: [recipient.user_id], - skipNavigation: true - }) - ) yield* put( make(Name.TIP_UNLOCKED_CHAT, { recipientUserId: recipient.user_id diff --git a/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx b/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx index 290eff6afe..86ce29ad0f 100644 --- a/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx +++ b/packages/web/src/components/inbox-unavailable-modal/InboxUnavailableModal.tsx @@ -4,9 +4,13 @@ import { CHAT_BLOG_POST_URL, ChatPermissionAction, User, + accountSelectors, + cacheUsersSelectors, chatActions, chatSelectors, - tippingActions + makeChatId, + tippingActions, + useInboxUnavailableModal } from '@audius/common' import { IconMessageLocked, @@ -20,6 +24,7 @@ import { ButtonType, IconUnblockMessages } from '@audius/stems' +import { Action } from '@reduxjs/toolkit' import { useDispatch } from 'react-redux' import { UserNameAndBadges } from 'components/user-name-and-badges/UserNameAndBadges' @@ -42,14 +47,19 @@ const messages = { ), tipButton: 'Send $AUDIO', unblockContent: 'You cannot send messages to users you have blocked.', - unblockButton: 'Unblock' + unblockButton: 'Unblock', + defaultUsername: 'this user' } -const actionToContent = ( - action: ChatPermissionAction, - user: User, +const actionToContent = ({ + action, + user, + onClose +}: { + action: ChatPermissionAction + user?: User | null onClose: () => void -) => { +}) => { switch (action) { case ChatPermissionAction.NONE: return { @@ -60,7 +70,11 @@ const actionToContent = ( case ChatPermissionAction.TIP: return { content: messages.tipContent( - + user ? ( + + ) : ( + messages.defaultUsername + ) ), buttonText: messages.tipButton, buttonIcon: @@ -83,57 +97,94 @@ const actionToContent = ( const { beginTip } = tippingActions const { getCanCreateChat } = chatSelectors -export const InboxUnavailableModal = ({ - isVisible, - onDismiss, - onClose, - user -}: { - isVisible: boolean - onDismiss?: () => void - onClose: () => void - user: User -}) => { +export const InboxUnavailableModal = () => { + const { isOpen, onClose, onClosed, data } = useInboxUnavailableModal() + const { userId, presetMessage, onSuccessAction, onCancelAction } = data + const user = useSelector((state) => + cacheUsersSelectors.getUser(state, { id: userId }) + ) const dispatch = useDispatch() + const currentUserId = useSelector(accountSelectors.getUserId) const { callToAction } = useSelector((state) => - getCanCreateChat(state, { userId: user.user_id }) + getCanCreateChat(state, { userId }) ) const hasAction = callToAction === ChatPermissionAction.TIP || callToAction === ChatPermissionAction.UNBLOCK const handleClick = useCallback(() => { - if (callToAction === ChatPermissionAction.TIP) { - dispatch(beginTip({ user, source: 'inboxUnavailableModal' })) - onClose() + if (!userId) { + console.error( + 'Unexpected undefined user for InboxUnavailableModal click handler' + ) + return + } + if (callToAction === ChatPermissionAction.TIP && currentUserId) { + const chatId = makeChatId([currentUserId, userId]) + const tipSuccessActions: Action[] = [ + chatActions.goToChat({ + chatId, + presetMessage + }) + ] + if (onSuccessAction) { + tipSuccessActions.push(onSuccessAction) + } + dispatch( + beginTip({ + user, + source: 'inboxUnavailableModal', + onSuccessActions: tipSuccessActions, + onSuccessConfirmedActions: [ + chatActions.createChat({ + userIds: [userId], + skipNavigation: true + }) + ] + }) + ) } else if (callToAction === ChatPermissionAction.UNBLOCK) { - dispatch(unblockUser({ userId: user.user_id })) - dispatch(createChat({ userIds: [user.user_id] })) - onClose() + dispatch(unblockUser({ userId })) + dispatch(createChat({ userIds: [userId], presetMessage })) + if (onSuccessAction) { + dispatch(onSuccessAction) + } } else { window.open(CHAT_BLOG_POST_URL, '_blank') - onClose() } - }, [dispatch, onClose, user, callToAction]) + onClose() + }, [ + user, + userId, + callToAction, + currentUserId, + onClose, + presetMessage, + onSuccessAction, + dispatch + ]) - const handleDismiss = useCallback(() => { - onDismiss?.() + const handleCancel = useCallback(() => { + if (onCancelAction) { + dispatch(onCancelAction) + } onClose() - }, [onDismiss, onClose]) + }, [dispatch, onCancelAction, onClose]) - const { content, buttonText, buttonIcon } = actionToContent( - callToAction, + const { content, buttonText, buttonIcon } = actionToContent({ + action: callToAction, user, onClose - ) + }) return ( - + } title={messages.title} diff --git a/packages/web/src/components/search-users-modal/SearchUsersModal.tsx b/packages/web/src/components/search-users-modal/SearchUsersModal.tsx index 8ffc80ea36..9a9e4ae1dd 100644 --- a/packages/web/src/components/search-users-modal/SearchUsersModal.tsx +++ b/packages/web/src/components/search-users-modal/SearchUsersModal.tsx @@ -1,8 +1,14 @@ -import { ChangeEvent, ReactNode, useCallback, useRef, useState } from 'react' +import { + ChangeEvent, + ReactNode, + useCallback, + useEffect, + useRef, + useState +} from 'react' import { cacheUsersSelectors, - Modals, searchUsersModalSelectors, searchUsersModalActions, useProxySelector, @@ -16,6 +22,7 @@ import { IconSearch, Modal, ModalHeader, + ModalProps, ModalTitle, ModalTitleProps, Scrollbar @@ -24,7 +31,6 @@ import InfiniteScroll from 'react-infinite-scroller' import { useDispatch, useSelector } from 'react-redux' import { useDebounce } from 'react-use' -import { useModalState } from 'common/hooks/useModalState' import { InputV2, InputV2Size, @@ -41,11 +47,10 @@ const messages = { const DEBOUNCE_MS = 100 const { searchUsers } = searchUsersModalActions -const { getUserList } = searchUsersModalSelectors +const { getUserList, getLastSearchQuery } = searchUsersModalSelectors const { getUsers } = cacheUsersSelectors type SearchUsersModalProps = { - modalName: Modals titleProps: ModalTitleProps debounceMs?: number defaultUserList?: { @@ -57,12 +62,10 @@ type SearchUsersModalProps = { renderEmpty?: () => ReactNode renderUser: (user: User, closeParentModal: () => void) => ReactNode onCancel?: () => void - onClose?: () => void -} +} & Omit export const SearchUsersModal = (props: SearchUsersModalProps) => { const { - modalName, titleProps, debounceMs = DEBOUNCE_MS, defaultUserList = { @@ -73,23 +76,25 @@ export const SearchUsersModal = (props: SearchUsersModalProps) => { }, renderUser, renderEmpty = () => null, + isOpen, onClose, + onClosed, onCancel } = props const dispatch = useDispatch() - const [isVisible, setIsVisible] = useModalState(modalName) const [query, setQuery] = useState('') const [hasQuery, setHasQuery] = useState(false) const scrollParentRef = useRef(null) const { userIds, hasMore, status } = useSelector(getUserList) + const lastSearchQuery = useSelector(getLastSearchQuery) const users = useProxySelector( (state) => { const ids = hasQuery ? userIds : defaultUserList.userIds const users = getUsers(state, { ids }) return ids.map((id) => users[id]) }, - [hasQuery, userIds, isVisible] + [hasQuery, userIds, isOpen] ) useDebounce( @@ -102,10 +107,9 @@ export const SearchUsersModal = (props: SearchUsersModalProps) => { ) const handleClose = useCallback(() => { - setIsVisible(false) setQuery('') - onClose?.() - }, [setIsVisible, onClose]) + onClose() + }, [onClose]) const handleCancel = useCallback(() => { handleClose() @@ -130,8 +134,15 @@ export const SearchUsersModal = (props: SearchUsersModalProps) => { } }, [hasQuery, query, status, defaultUserList, dispatch]) + // Clear the query if something else resets our search state + useEffect(() => { + if (!lastSearchQuery) { + setQuery('') + } + }, [lastSearchQuery, setQuery]) + return ( - + diff --git a/packages/web/src/components/share-modal/ShareModal.tsx b/packages/web/src/components/share-modal/ShareModal.tsx index 74c7c29290..f09667daa4 100644 --- a/packages/web/src/components/share-modal/ShareModal.tsx +++ b/packages/web/src/components/share-modal/ShareModal.tsx @@ -9,9 +9,9 @@ import { usersSocialActions, shareModalUISelectors, shareSoundToTiktokModalActions, - createChatModalActions, modalsActions, - PlayableType + PlayableType, + useCreateChatModal } from '@audius/common' import { useDispatch } from 'react-redux' @@ -36,7 +36,6 @@ const { shareTrack } = tracksSocialActions const { shareAudioNftPlaylist, shareCollection } = collectionsSocialActions const getAccountUser = accountSelectors.getAccountUser const { setVisibility } = modalsActions -const { setState: setCreateChatModalState } = createChatModalActions export const ShareModal = () => { const { isOpen, onClose, onClosed } = useModalState('Share') @@ -46,6 +45,7 @@ export const ShareModal = () => { const record = useRecord() const { content, source } = useSelector(getShareState) const account = useSelector(getAccountUser) + const { onOpen: openCreateChatModal } = useCreateChatModal() const { isEnabled: isShareSoundToTikTokEnabled } = useFlag( FeatureFlags.SHARE_SOUND_TO_TIKTOK @@ -57,16 +57,13 @@ export const ShareModal = () => { const handleShareToDirectMessage = useCallback(async () => { if (!content) return onClose() - dispatch(setVisibility({ modal: 'CreateChat', visible: true })) - dispatch( - setCreateChatModalState({ - // Just care about the link - presetMessage: (await getTwitterShareText(content, false)).link, - onCancelAction: setVisibility({ modal: 'Share', visible: true }) - }) - ) + openCreateChatModal({ + // Just care about the link + presetMessage: (await getTwitterShareText(content, false)).link, + onCancelAction: setVisibility({ modal: 'Share', visible: true }) + }) dispatch(make(Name.CHAT_ENTRY_POINT, { source: 'share' })) - }, [dispatch, onClose, content]) + }, [openCreateChatModal, dispatch, onClose, content]) const handleShareToTwitter = useCallback(async () => { if (!source || !content) return diff --git a/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx b/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx index 508a135126..bc893a2346 100644 --- a/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx +++ b/packages/web/src/components/tipping/tip-audio/TipAudioModal.tsx @@ -7,9 +7,7 @@ import { TippingSendStatus, walletActions, StringKeys, - chatActions, - accountSelectors, - makeChatId + accountSelectors } from '@audius/common' import { Modal, ModalHeader, ModalTitle } from '@audius/stems' import cn from 'classnames' @@ -114,7 +112,7 @@ export const TipAudioModal = () => { const dispatch = useDispatch() const sendStatus = useSelector(getSendStatus) const previousSendStatus = usePrevious(sendStatus) - const { user: recipient, source } = useSelector(getSendTipData) + const { user: recipient, onSuccessActions } = useSelector(getSendTipData) const currentUserId = useSelector(accountSelectors.getUserId) const audioFeaturesDegradedText = useRemoteVar( @@ -126,19 +124,23 @@ export const TipAudioModal = () => { // attempting to make if they were unlocking DMs by tipping. // The saga will create the chat once the tip is confirmed if ( - source === 'inboxUnavailableModal' && + onSuccessActions && sendStatus === 'SUCCESS' && recipient?.user_id && currentUserId ) { - dispatch( - chatActions.goToChat({ - chatId: makeChatId([currentUserId, recipient.user_id]) - }) - ) + for (const action of onSuccessActions) { + dispatch(action) + } } dispatch(resetSend()) - }, [currentUserId, dispatch, recipient?.user_id, sendStatus, source]) + }, [ + currentUserId, + dispatch, + onSuccessActions, + recipient?.user_id, + sendStatus + ]) useEffect(() => { if (sendStatus !== null) { diff --git a/packages/web/src/pages/chat-page/components/ChatHeader.tsx b/packages/web/src/pages/chat-page/components/ChatHeader.tsx index b2d41bcaff..1f1283c837 100644 --- a/packages/web/src/pages/chat-page/components/ChatHeader.tsx +++ b/packages/web/src/pages/chat-page/components/ChatHeader.tsx @@ -1,6 +1,11 @@ import { forwardRef, useCallback, useState } from 'react' -import { useProxySelector, chatSelectors, User } from '@audius/common' +import { + useProxySelector, + chatSelectors, + User, + useCreateChatModal +} from '@audius/common' import { IconButton, IconCompose, @@ -53,7 +58,7 @@ export const ChatHeader = forwardRef( ref ) => { const dispatch = useDispatch() - const [, setCreateChatVisible] = useModalState('CreateChat') + const { onOpen: openCreateChatModal } = useCreateChatModal() const [, setInboxSettingsVisible] = useModalState('InboxSettings') const [isUnblockUserModalVisible, setIsUnblockUserModalVisible] = useState(false) @@ -71,8 +76,8 @@ export const ChatHeader = forwardRef( const isBlocked = user && blockeeList.includes(user.user_id) const handleComposeClicked = useCallback(() => { - setCreateChatVisible(true) - }, [setCreateChatVisible]) + openCreateChatModal() + }, [openCreateChatModal]) const handleSettingsClicked = useCallback(() => { setInboxSettingsVisible(true) diff --git a/packages/web/src/pages/chat-page/components/CreateChatModal.tsx b/packages/web/src/pages/chat-page/components/CreateChatModal.tsx index 6cb99e840f..f0f32ba62b 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatModal.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatModal.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect } from 'react' import { accountSelectors, @@ -8,17 +8,17 @@ import { followersUserListActions, followersUserListSelectors, User, - createChatModalSelectors, - createChatModalActions + useCreateChatModal, + useInboxUnavailableModal, + createChatModalActions, + searchUsersModalActions } from '@audius/common' import { IconCompose } from '@audius/stems' import { useDispatch } from 'react-redux' -import { useModalState } from 'common/hooks/useModalState' import { useSelector } from 'common/hooks/useSelector' -import { InboxUnavailableModal } from 'components/inbox-unavailable-modal/InboxUnavailableModal' import { SearchUsersModal } from 'components/search-users-modal/SearchUsersModal' -import { MessageUserSearchResult } from 'pages/chat-page/components/CreateChatUserResult' +import { CreateChatUserResult } from 'pages/chat-page/components/CreateChatUserResult' import { CreateChatEmptyResults } from './CreateChatEmptyResults' @@ -28,34 +28,22 @@ const messages = { const { getAccountUser } = accountSelectors const { fetchBlockers } = chatActions -const { getOnCancelAction, getPresetMessage } = createChatModalSelectors -const { setState } = createChatModalActions - -const CREATE_CHAT_MODAL = 'CreateChat' export const CreateChatModal = () => { const dispatch = useDispatch() const currentUser = useSelector(getAccountUser) - const [isVisible, setIsVisible] = useModalState(CREATE_CHAT_MODAL) - const onCancelAction = useSelector(getOnCancelAction) - const presetMessage = useSelector(getPresetMessage) - const [user, setUser] = useState() - const [showInboxUnavailableModal, setShowInboxUnavailableModal] = - useState(false) + const { isOpen, onClose, onClosed, data } = useCreateChatModal() + const { onOpen: openInboxUnavailableModal } = useInboxUnavailableModal() + const { onCancelAction, presetMessage } = data const { userIds, loading, hasMore } = useSelector( followersUserListSelectors.getUserList ) - const handleClose = useCallback(() => { - dispatch(setState({})) - }, [dispatch]) - const handleCancel = useCallback(() => { if (onCancelAction) { dispatch(onCancelAction) } - dispatch(setState({})) }, [onCancelAction, dispatch]) const loadMore = useCallback(() => { @@ -67,35 +55,30 @@ export const CreateChatModal = () => { const handleOpenInboxUnavailableModal = useCallback( (user: User) => { - setShowInboxUnavailableModal(true) - setUser(user) - setIsVisible(false) + openInboxUnavailableModal({ + userId: user.user_id, + presetMessage, + onSuccessAction: searchUsersModalActions.searchUsers({ query: '' }), + onCancelAction: createChatModalActions.open(data) + }) + onClose() }, - [setShowInboxUnavailableModal, setUser, setIsVisible] + [data, presetMessage, openInboxUnavailableModal, onClose] ) - const handleDismissInboxUnavailableModal = useCallback(() => { - setIsVisible(true) - }, [setIsVisible]) - - const handleCloseInboxUnavailableModal = useCallback(() => { - setShowInboxUnavailableModal(false) - }, [setShowInboxUnavailableModal]) - useEffect(() => { loadMore() }, [loadMore]) useEffect(() => { - if (isVisible) { + if (isOpen) { dispatch(fetchBlockers()) } - }, [dispatch, isVisible]) + }, [dispatch, isOpen]) return ( <> }} defaultUserList={{ userIds, @@ -104,7 +87,7 @@ export const CreateChatModal = () => { hasMore }} renderUser={(user, closeParentModal) => ( - { /> )} renderEmpty={() => } - onClose={handleClose} + isOpen={isOpen} + onClose={onClose} + onClosed={onClosed} onCancel={handleCancel} /> - {user ? ( - - ) : null} ) } diff --git a/packages/web/src/pages/chat-page/components/CreateChatPrompt.tsx b/packages/web/src/pages/chat-page/components/CreateChatPrompt.tsx index fe11721ab0..70bdf99410 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatPrompt.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatPrompt.tsx @@ -1,15 +1,13 @@ import { useCallback } from 'react' -import { chatSelectors, modalsActions } from '@audius/common' +import { chatSelectors, useCreateChatModal } from '@audius/common' import { Button, ButtonType, IconCompose } from '@audius/stems' -import { useDispatch } from 'react-redux' import { useSelector } from 'common/hooks/useSelector' import styles from './CreateChatPrompt.module.css' const { getChats } = chatSelectors -const { setVisibility } = modalsActions const messages = { selectTitle: 'Select a Message', @@ -21,13 +19,13 @@ const messages = { } export const CreateChatPrompt = () => { - const dispatch = useDispatch() + const { onOpen: openCreateChatModal } = useCreateChatModal() const chats = useSelector(getChats) const hasChats = chats?.length > 0 const handleClick = useCallback(() => { - dispatch(setVisibility({ modal: 'CreateChat', visible: true })) - }, [dispatch]) + openCreateChatModal() + }, [openCreateChatModal]) return (
diff --git a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx index 7bcf476ba0..bd2142c986 100644 --- a/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx +++ b/packages/web/src/pages/chat-page/components/CreateChatUserResult.tsx @@ -92,7 +92,7 @@ const renderCustomChip = (callToAction: ChatPermissionAction) => { } } -export const MessageUserSearchResult = (props: UserResultComposeProps) => { +export const CreateChatUserResult = (props: UserResultComposeProps) => { const dispatch = useDispatch() const { user, closeParentModal, openInboxUnavailableModal, presetMessage } = props diff --git a/packages/web/src/pages/modals/Modals.tsx b/packages/web/src/pages/modals/Modals.tsx index 4f25f477f9..37196df37d 100644 --- a/packages/web/src/pages/modals/Modals.tsx +++ b/packages/web/src/pages/modals/Modals.tsx @@ -21,6 +21,7 @@ import EmbedModal from 'components/embed-modal/EmbedModal' import { FeatureFlagOverrideModal } from 'components/feature-flag-override-modal' import FirstUploadModal from 'components/first-upload-modal/FirstUploadModal' import { InboxSettingsModal } from 'components/inbox-settings-modal/InboxSettingsModal' +import { InboxUnavailableModal } from 'components/inbox-unavailable-modal/InboxUnavailableModal' import { LeavingAudiusModal } from 'components/leaving-audius-modal/LeavingAudiusModal' import { LockedContentModal } from 'components/locked-content-modal/LockedContentModal' import PasswordResetModal from 'components/password-reset/PasswordResetModal' @@ -96,6 +97,7 @@ const Modals = () => { + {!isMobileClient && ( <> diff --git a/packages/web/src/pages/profile-page/ProfilePageProvider.tsx b/packages/web/src/pages/profile-page/ProfilePageProvider.tsx index 4871791513..fc1245d5c4 100644 --- a/packages/web/src/pages/profile-page/ProfilePageProvider.tsx +++ b/packages/web/src/pages/profile-page/ProfilePageProvider.tsx @@ -36,7 +36,8 @@ import { Nullable, chatActions, chatSelectors, - ChatPermissionAction + ChatPermissionAction, + inboxUnavailableModalActions } from '@audius/common' import { push as pushRoute, replace } from 'connected-react-router' import { UnregisterCallback } from 'history' @@ -122,7 +123,6 @@ type ProfilePageState = { updatedDonation: string | null tracksLineupOrder: TracksSortMode areArtistRecommendationsVisible: boolean - showInboxUnavailableModal: boolean showBlockUserConfirmationModal: boolean showUnblockUserConfirmationModal: boolean } @@ -138,7 +138,6 @@ class ProfilePage extends PureComponent { shouldMaskContent: false, tracksLineupOrder: TracksSortMode.RECENT, areArtistRecommendationsVisible: false, - showInboxUnavailableModal: false, showBlockUserConfirmationModal: false, showUnblockUserConfirmationModal: false, ...INITIAL_UPDATE_FIELDS @@ -278,10 +277,6 @@ class ProfilePage extends PureComponent { this.setState({ areArtistRecommendationsVisible: false }) } - onCloseInboxUnavailableModal = () => { - this.setState({ showInboxUnavailableModal: false }) - } - onCloseBlockUserConfirmationModal = () => { this.setState({ showBlockUserConfirmationModal: false }) } @@ -743,8 +738,8 @@ class ProfilePage extends PureComponent { if (this.props.chatPermissions?.canCreateChat) { return this.props.onMessage(profile!.user_id) - } else { - this.setState({ showInboxUnavailableModal: true }) + } else if (profile) { + this.props.onShowInboxUnavailableModal(profile.user_id) } } @@ -995,8 +990,6 @@ class ProfilePage extends PureComponent { this.props.chatPermissions.canCreateChat || this.props.chatPermissions.callToAction === ChatPermissionAction.SIGN_UP, - showInboxUnavailableModal: this.state.showInboxUnavailableModal, - onCloseInboxUnavailableModal: this.onCloseInboxUnavailableModal, showBlockUserConfirmationModal: this.state.showBlockUserConfirmationModal, onCloseBlockUserConfirmationModal: this.onCloseBlockUserConfirmationModal, showUnblockUserConfirmationModal: @@ -1221,6 +1214,9 @@ function mapDispatchToProps(dispatch: Dispatch, props: RouteComponentProps) { redirectUnauthenticatedAction: () => { dispatch(openSignOn()) dispatch(showRequiresAccountModal()) + }, + onShowInboxUnavailableModal: (userId: ID) => { + dispatch(inboxUnavailableModalActions.open({ 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 6b4d0f14ed..31854476bb 100644 --- a/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx +++ b/packages/web/src/pages/profile-page/components/desktop/ProfilePage.tsx @@ -26,7 +26,6 @@ import { make, useRecord } from 'common/store/analytics/actions' import Card from 'components/card/desktop/Card' import CollectiblesPage from 'components/collectibles/components/CollectiblesPage' import CoverPhoto from 'components/cover-photo/CoverPhoto' -import { InboxUnavailableModal } from 'components/inbox-unavailable-modal/InboxUnavailableModal' import CardLineup from 'components/lineup/CardLineup' import Lineup from 'components/lineup/Lineup' import Mask from 'components/mask/Mask' @@ -92,7 +91,6 @@ export type ProfilePageProps = { stats: StatProps[] isBlocked: boolean canCreateChat: boolean - showInboxUnavailableModal: boolean showBlockUserConfirmationModal: boolean showUnblockUserConfirmationModal: boolean @@ -155,7 +153,6 @@ export type ProfilePageProps = { onMessage: () => void onBlock: () => void onUnblock: () => void - onCloseInboxUnavailableModal: () => void onCloseBlockUserConfirmationModal: () => void onCloseUnblockUserConfirmationModal: () => void } @@ -217,8 +214,6 @@ const ProfilePage = ({ isBlocked, // Chat modals - showInboxUnavailableModal, - onCloseInboxUnavailableModal, showBlockUserConfirmationModal, onCloseBlockUserConfirmationModal, showUnblockUserConfirmationModal, @@ -811,11 +806,6 @@ const ProfilePage = ({
{profile ? ( <> -