From 01fcab4241d12018ca7b647c63d5ffffc191f5ff Mon Sep 17 00:00:00 2001 From: Reed <3893871+dharit-tan@users.noreply.github.com> Date: Tue, 23 May 2023 16:54:09 -0400 Subject: [PATCH] [PAY-1195] Mobile chat screen unavailable states (#3402) --- packages/common/src/store/pages/chat/sagas.ts | 21 ++- .../common/src/store/pages/chat/selectors.ts | 25 ++-- packages/common/src/store/pages/chat/slice.ts | 6 + .../src/screens/chat-screen/ChatScreen.tsx | 55 +++++-- .../screens/chat-screen/ChatUnavailable.tsx | 135 ++++++++++++++++++ 5 files changed, 215 insertions(+), 27 deletions(-) create mode 100644 packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx diff --git a/packages/common/src/store/pages/chat/sagas.ts b/packages/common/src/store/pages/chat/sagas.ts index 4085ea544c5..b2e9f8aea3f 100644 --- a/packages/common/src/store/pages/chat/sagas.ts +++ b/packages/common/src/store/pages/chat/sagas.ts @@ -34,6 +34,7 @@ const { fetchUnreadMessagesCountSucceeded, fetchUnreadMessagesCountFailed, goToChat, + fetchChatIfNecessary, fetchMoreChats, fetchMoreChatsSucceeded, fetchMoreChatsFailed, @@ -121,7 +122,7 @@ function* doFetchMoreMessages(action: ReturnType) { const sdk = yield* call(audiusSdk) // Ensure we get a chat so we can check the unread count - yield* call(fetchChatIfNecessary, { chatId }) + yield* call(doFetchChatIfNecessary, { chatId }) const chat = yield* select((state) => getChat(state, chatId)) // Paginate through messages until we get to the unread indicator @@ -211,7 +212,7 @@ function* doCreateChat(action: ReturnType) { .sort() .join(':') try { - yield* call(fetchChatIfNecessary, { chatId }) + yield* call(doFetchChatIfNecessary, { chatId }) } catch {} const existingChat = yield* select((state) => getChat(state, chatId)) if (existingChat) { @@ -293,10 +294,13 @@ function* doSendMessage(action: ReturnType) { } } -function* fetchChatIfNecessary(args: { chatId: string }) { - const { chatId } = args +function* doFetchChatIfNecessary(args: { + chatId: string + bustCache?: boolean +}) { + const { chatId, bustCache = false } = args const existingChat = yield* select((state) => getChat(state, chatId)) - if (!existingChat) { + if (!existingChat || bustCache) { const audiusSdk = yield* getContext('audiusSdk') const sdk = yield* call(audiusSdk) const { data: chat } = yield* call([sdk.chats, sdk.chats.get], { chatId }) @@ -441,6 +445,12 @@ function* watchAddMessage() { yield takeEvery(addMessage, ({ payload }) => fetchChatIfNecessary(payload)) } +function* watchFetchChatIfNecessary() { + yield takeEvery(fetchChatIfNecessary, ({ payload }) => + doFetchChatIfNecessary(payload) + ) +} + function* watchSendMessage() { yield takeEvery(sendMessage, doSendMessage) } @@ -496,6 +506,7 @@ function* watchDeleteChat() { export const sagas = () => { return [ watchFetchUnreadMessagesCount, + watchFetchChatIfNecessary, watchFetchChats, watchFetchChatMessages, watchSetMessageReaction, diff --git a/packages/common/src/store/pages/chat/selectors.ts b/packages/common/src/store/pages/chat/selectors.ts index c885467a1a1..f7ea6c08de1 100644 --- a/packages/common/src/store/pages/chat/selectors.ts +++ b/packages/common/src/store/pages/chat/selectors.ts @@ -193,14 +193,18 @@ export const getCanCreateChat = createSelector( getBlockees, getBlockers, getChatPermissions, - (_: CommonState, { userId }: { userId: Maybe }) => userId + (state: CommonState, { userId }: { userId: Maybe }) => { + if (!userId) return null + const usersMap = getUsers(state, { ids: [userId] }) + return usersMap[userId] + } ], ( currentUserId, blockees, blockers, chatPermissions, - userId + user ): { canCreateChat: boolean; callToAction: ChatPermissionAction } => { if (!currentUserId) { return { @@ -208,16 +212,16 @@ export const getCanCreateChat = createSelector( callToAction: ChatPermissionAction.SIGN_UP } } - if (!userId) { + if (!user) { return { canCreateChat: false, callToAction: ChatPermissionAction.NONE } } - const userPermissions = chatPermissions[userId] - const isBlockee = blockees.includes(userId) - const isBlocker = blockers.includes(userId) + const userPermissions = chatPermissions[user.user_id] + const isBlockee = blockees.includes(user.user_id) + const isBlocker = blockers.includes(user.user_id) const canCreateChat = !isBlockee && !isBlocker && @@ -229,10 +233,15 @@ export const getCanCreateChat = createSelector( action = ChatPermissionAction.WAIT } else if ( userPermissions.permits === ChatPermission.NONE || - blockers.includes(userId) + blockers.includes(user.user_id) + ) { + action = ChatPermissionAction.NONE + } else if ( + userPermissions.permits === ChatPermission.FOLLOWEES && + !user?.does_current_user_follow ) { action = ChatPermissionAction.NONE - } else if (blockees.includes(userId)) { + } else if (blockees.includes(user.user_id)) { action = ChatPermissionAction.UNBLOCK } else if (userPermissions.permits === ChatPermission.TIPPERS) { action = ChatPermissionAction.TIP diff --git a/packages/common/src/store/pages/chat/slice.ts b/packages/common/src/store/pages/chat/slice.ts index 154c627e9d9..0926bd51a57 100644 --- a/packages/common/src/store/pages/chat/slice.ts +++ b/packages/common/src/store/pages/chat/slice.ts @@ -295,6 +295,12 @@ const slice = createSlice({ const { messageId } = action.payload delete state.optimisticReactions[messageId] }, + fetchChatIfNecessary: ( + _state, + _action: PayloadAction<{ chatId: string; bustCache?: boolean }> + ) => { + // triggers saga + }, fetchChatSucceeded: (state, action: PayloadAction<{ chat: UserChat }>) => { const { chat } = action.payload if (dayjs(chat.cleared_history_at).isAfter(chat.last_message_at)) { diff --git a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx index dd49543c79e..5654a1b7574 100644 --- a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx @@ -42,6 +42,7 @@ import type { AppTabScreenParamList } from '../app-screen' import { ChatMessageListItem } from './ChatMessageListItem' import { ChatTextInput } from './ChatTextInput' +import { ChatUnavailable } from './ChatUnavailable' import { EmptyChatMessages } from './EmptyChatMessages' import { ReactionPopup } from './ReactionPopup' @@ -51,11 +52,18 @@ const { getChat, getChatMessageById, getChatMessageByIndex, - getReactionsPopupMessageId + getReactionsPopupMessageId, + getCanSendMessage } = chatSelectors - -const { fetchMoreMessages, markChatAsRead, setReactionsPopupMessageId } = - chatActions +const { + fetchMoreMessages, + markChatAsRead, + setReactionsPopupMessageId, + fetchBlockers, + fetchBlockees, + fetchPermissions, + fetchChatIfNecessary +} = chatActions const { getUserId } = accountSelectors const { getHasTrack } = playerSelectors @@ -213,6 +221,9 @@ export const ChatScreen = () => { const popupMessage = useSelector((state) => getChatMessageById(state, chatId ?? '', popupMessageId ?? '') ) + const { canSendMessage } = useSelector((state) => + getCanSendMessage(state, { userId: otherUser.user_id, chatId }) + ) // A ref so that the unread separator doesn't disappear immediately when the chat is marked as read // Using a ref instead of state here to prevent unwanted flickers. @@ -237,6 +248,18 @@ export const ChatScreen = () => { } }, [chatId, chat]) + // Fetch all permissions, blockers/blockees, and recheck_permissions flag + useEffect(() => { + dispatch(fetchBlockees()) + dispatch(fetchBlockers()) + if (otherUser.user_id) { + dispatch(fetchPermissions({ userIds: [otherUser.user_id] })) + } + if (chatId) { + dispatch(fetchChatIfNecessary({ chatId, bustCache: true })) + } + }, [chatId, dispatch, otherUser.user_id]) + // Find earliest unread message to display unread tag correctly const earliestUnreadIndex = useMemo( () => @@ -434,7 +457,7 @@ export const ChatScreen = () => { {/* Everything inside the portal displays on top of all other screen contents. */} - {shouldShowPopup && popupMessage ? ( + {canSendMessage && shouldShowPopup && popupMessage ? ( { )} - - - - + {canSendMessage ? ( + + + + + ) : ( + + )} diff --git a/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx b/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx new file mode 100644 index 00000000000..d1074d00ad3 --- /dev/null +++ b/packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx @@ -0,0 +1,135 @@ +import { useCallback, useMemo } from 'react' + +import { chatSelectors, ChatPermissionAction } from '@audius/common' +import { View, Text } from 'react-native' +import { useDispatch, useSelector } from 'react-redux' + +import { UserBadges } from 'app/components/user-badges' +import { useNavigation } from 'app/hooks/useNavigation' +import { setVisibility } from 'app/store/drawers/slice' +import { makeStyles } from 'app/styles' + +const { getCanSendMessage, getOtherChatUsers } = chatSelectors + +const messages = { + noAction: 'You can no longer send messages to ', + tip1: 'You must send ', + tip2: ' a tip before you can send them messages.', + blockee: 'You cannot send messages to users you have blocked. ', + learnMore: 'Learn More.', + unblockUser: 'Unblock User.' +} + +const useStyles = makeStyles(({ spacing, palette, typography }) => ({ + root: { + display: 'flex', + alignItems: 'center', + paddingBottom: spacing(19), + paddingHorizontal: spacing(6) + }, + unavailableText: { + textAlign: 'center', + fontSize: typography.fontSize.medium, + lineHeight: typography.fontSize.medium * 1.3, + color: palette.neutral + }, + link: { + color: palette.secondary + } +})) + +type ChatUnavailableProps = { + chatId: string +} + +export const ChatUnavailable = ({ chatId }: ChatUnavailableProps) => { + const styles = useStyles() + const dispatch = useDispatch() + const navigation = useNavigation() + const [otherUser] = useSelector((state) => getOtherChatUsers(state, chatId)) + const { callToAction } = useSelector((state) => + getCanSendMessage(state, { userId: otherUser.user_id, chatId }) + ) + + // TODO: link to blog + const handleLearnMorePress = useCallback(() => {}, []) + + const handleUnblockPress = useCallback(() => { + dispatch( + setVisibility({ + drawer: 'BlockMessages', + visible: true, + data: { userId: otherUser.user_id } + }) + ) + }, [dispatch, otherUser]) + + const mapChatPermissionActionToContent = useMemo(() => { + return { + [ChatPermissionAction.NONE]: () => ( + <> + + {messages.noAction} + + + {messages.learnMore} + + + + ), + [ChatPermissionAction.TIP]: () => ( + <> + + {messages.tip1} + + navigation.navigate('Profile', { id: otherUser.user_id }) + } + > + + + {messages.tip2} + + + ), + [ChatPermissionAction.UNBLOCK]: () => ( + <> + + {messages.blockee} + + {messages.unblockUser} + + + + ), + [ChatPermissionAction.WAIT]: () => null + } + }, [ + handleLearnMorePress, + handleUnblockPress, + navigation, + styles.link, + styles.unavailableText, + otherUser + ]) + + return ( + + {mapChatPermissionActionToContent[callToAction]()} + + ) +}