Skip to content

Commit

Permalink
[PAY-1195] Mobile chat screen unavailable states (#3402)
Browse files Browse the repository at this point in the history
  • Loading branch information
dharit-tan authored May 23, 2023
1 parent 67b1788 commit 01fcab4
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 27 deletions.
21 changes: 16 additions & 5 deletions packages/common/src/store/pages/chat/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const {
fetchUnreadMessagesCountSucceeded,
fetchUnreadMessagesCountFailed,
goToChat,
fetchChatIfNecessary,
fetchMoreChats,
fetchMoreChatsSucceeded,
fetchMoreChatsFailed,
Expand Down Expand Up @@ -121,7 +122,7 @@ function* doFetchMoreMessages(action: ReturnType<typeof fetchMoreMessages>) {
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
Expand Down Expand Up @@ -211,7 +212,7 @@ function* doCreateChat(action: ReturnType<typeof createChat>) {
.sort()
.join(':')
try {
yield* call(fetchChatIfNecessary, { chatId })
yield* call(doFetchChatIfNecessary, { chatId })
} catch {}
const existingChat = yield* select((state) => getChat(state, chatId))
if (existingChat) {
Expand Down Expand Up @@ -293,10 +294,13 @@ function* doSendMessage(action: ReturnType<typeof sendMessage>) {
}
}

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 })
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -496,6 +506,7 @@ function* watchDeleteChat() {
export const sagas = () => {
return [
watchFetchUnreadMessagesCount,
watchFetchChatIfNecessary,
watchFetchChats,
watchFetchChatMessages,
watchSetMessageReaction,
Expand Down
25 changes: 17 additions & 8 deletions packages/common/src/store/pages/chat/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,31 +193,35 @@ export const getCanCreateChat = createSelector(
getBlockees,
getBlockers,
getChatPermissions,
(_: CommonState, { userId }: { userId: Maybe<ID> }) => userId
(state: CommonState, { userId }: { userId: Maybe<ID> }) => {
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 {
canCreateChat: false,
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 &&
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/common/src/store/pages/chat/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
55 changes: 41 additions & 14 deletions packages/mobile/src/screens/chat-screen/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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

Expand Down Expand Up @@ -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.
Expand All @@ -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(
() =>
Expand Down Expand Up @@ -434,7 +457,7 @@ export const ChatScreen = () => {
<ScreenContent>
{/* Everything inside the portal displays on top of all other screen contents. */}
<Portal hostName='ChatReactionsPortal'>
{shouldShowPopup && popupMessage ? (
{canSendMessage && shouldShowPopup && popupMessage ? (
<ReactionPopup
chatId={chatId}
messageTop={messageTop.current}
Expand Down Expand Up @@ -494,15 +517,19 @@ export const ChatScreen = () => {
</View>
)}

<View
style={styles.composeView}
onLayout={measureChatContainerBottom}
ref={composeRef}
pointerEvents={'box-none'}
>
<View style={styles.whiteBackground} />
<ChatTextInput chatId={chatId} />
</View>
{canSendMessage ? (
<View
style={styles.composeView}
onLayout={measureChatContainerBottom}
ref={composeRef}
pointerEvents={'box-none'}
>
<View style={styles.whiteBackground} />
<ChatTextInput chatId={chatId} />
</View>
) : (
<ChatUnavailable chatId={chatId} />
)}
</KeyboardAvoidingView>
</View>
</ScreenContent>
Expand Down
135 changes: 135 additions & 0 deletions packages/mobile/src/screens/chat-screen/ChatUnavailable.tsx
Original file line number Diff line number Diff line change
@@ -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]: () => (
<>
<Text style={styles.unavailableText}>
{messages.noAction}
<UserBadges
user={otherUser}
as={Text}
nameStyle={styles.unavailableText}
/>
<Text
style={[styles.unavailableText, styles.link]}
onPress={handleLearnMorePress}
>
{messages.learnMore}
</Text>
</Text>
</>
),
[ChatPermissionAction.TIP]: () => (
<>
<Text style={styles.unavailableText}>
{messages.tip1}
<Text
onPress={() =>
navigation.navigate('Profile', { id: otherUser.user_id })
}
>
<UserBadges
user={otherUser}
as={Text}
nameStyle={[styles.unavailableText, styles.link]}
/>
</Text>
{messages.tip2}
</Text>
</>
),
[ChatPermissionAction.UNBLOCK]: () => (
<>
<Text style={styles.unavailableText}>
{messages.blockee}
<Text
style={[styles.unavailableText, styles.link]}
onPress={handleUnblockPress}
>
{messages.unblockUser}
</Text>
</Text>
</>
),
[ChatPermissionAction.WAIT]: () => null
}
}, [
handleLearnMorePress,
handleUnblockPress,
navigation,
styles.link,
styles.unavailableText,
otherUser
])

return (
<View style={styles.root}>
{mapChatPermissionActionToContent[callToAction]()}
</View>
)
}

0 comments on commit 01fcab4

Please sign in to comment.