Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '~/api'
import {
decodeHashId,
getChatBlastAudienceDescription,
getChatBlastCTA,
getChatBlastSecondaryTitle,
getChatBlastTitle
Expand Down Expand Up @@ -95,11 +96,15 @@ export const useChatBlastAudienceContent = ({ chat }: { chat: ChatBlast }) => {
audience,
audienceContentId
})
const chatBlastAudienceDescription = getChatBlastAudienceDescription({
audience
})
const chatBlastCTA = getChatBlastCTA({ audience, audienceContentId })

return {
chatBlastTitle,
chatBlastSecondaryTitle,
chatBlastAudienceDescription,
chatBlastCTA,
contentTitle,
audienceCount
Expand Down
21 changes: 21 additions & 0 deletions packages/common/src/utils/chatUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const messages = {
blastTitleRemixers: 'Remix Creators',
blastTitleCustomers2: 'All Purchasers',
blastTitleRemixers2: 'Remixed',
blastFollowersDescription: 'Everyone who follows you.',
blastSupportersDescription: 'Everyone who has sent you a tip.',
blastCustomersDescription: 'Everyone who has paid for your content.',
blastRemixersDescription: 'Everyone who has remixed your tracks.',
blastCTABase: 'Send a message blast to ',
blastCTAFollowers: 'each of your followers',
blastCTASupporters: 'everyone who has sent you a tip',
Expand Down Expand Up @@ -210,6 +214,23 @@ export const getChatBlastSecondaryTitle = ({
}
}

export const getChatBlastAudienceDescription = ({
audience
}: {
audience: ChatBlastAudience
}) => {
switch (audience) {
case ChatBlastAudience.FOLLOWERS:
return messages.blastFollowersDescription
case ChatBlastAudience.TIPPERS:
return messages.blastSupportersDescription
case ChatBlastAudience.CUSTOMERS:
return messages.blastCustomersDescription
case ChatBlastAudience.REMIXERS:
return messages.blastRemixersDescription
}
}

export const getChatBlastCTA = ({
audience,
audienceContentId
Expand Down
3 changes: 2 additions & 1 deletion packages/common/src/utils/hashIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const MIN_LENGTH = 5
const hashids = new Hashids(HASH_SALT, MIN_LENGTH)

/** Decodes a string id into an int. Returns null if an invalid ID. */
export const decodeHashId = (id: string): Nullable<number> => {
export const decodeHashId = (id?: string): Nullable<number> => {
if (!id) return null
try {
const ids = hashids.decode(id)
if (!ids.length) return null
Expand Down
25 changes: 25 additions & 0 deletions packages/mobile/src/screens/chat-screen/ChatBlastHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useChatBlastAudienceContent } from '@audius/common/hooks'
import type { ChatBlast } from '@audius/sdk'

import { Flex, Text, IconTowerBroadcast } from '@audius/harmony-native'

const messages = {
chatBlastTitleAudienceCount: (audienceCount: number) => `(${audienceCount})`
Comment thread
dharit-tan marked this conversation as resolved.
}

export const ChatBlastHeader = ({ chat }: { chat: ChatBlast }) => {
const { chatBlastTitle, audienceCount } = useChatBlastAudienceContent({
chat
})
return (
<Flex row gap='s'>
<IconTowerBroadcast color='default' />
<Text variant='title'>{chatBlastTitle}</Text>
{audienceCount ? (
<Text variant='title' color='subdued'>
{messages.chatBlastTitleAudienceCount(audienceCount)}
</Text>
) : null}
</Flex>
)
}
44 changes: 44 additions & 0 deletions packages/mobile/src/screens/chat-screen/ChatBlastSubHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useChatBlastAudienceContent } from '@audius/common/hooks'
import { SquareSizes } from '@audius/common/models'
import { decodeHashId } from '@audius/common/utils'
import type { ChatBlast } from '@audius/sdk'

import { Flex, Text } from '@audius/harmony-native'
import { CollectionImageV2 } from 'app/components/image/CollectionImageV2'
import { TrackImageV2 } from 'app/components/image/TrackImageV2'

export const ChatBlastSubHeader = ({ chat }: { chat: ChatBlast }) => {
const {
audience_content_id: audienceContentId,
audience_content_type: audienceContentType
} = chat
const { chatBlastAudienceDescription, contentTitle } =
useChatBlastAudienceContent({ chat })
const decodedId = decodeHashId(audienceContentId) ?? undefined
return (
<Flex row backgroundColor='white' justifyContent='center' pb='s'>
{decodedId ? (
<Flex row gap='xs'>
{audienceContentType === 'track' ? (
<TrackImageV2
trackId={decodedId}
size={SquareSizes.SIZE_150_BY_150}
/>
) : (
<CollectionImageV2
collectionId={decodedId}
size={SquareSizes.SIZE_150_BY_150}
/>
)}
<Text variant='body' size='s' strength='strong'>
{contentTitle}
</Text>
</Flex>
) : (
<Text variant='body' size='s' color='subdued'>
{chatBlastAudienceDescription}
</Text>
)}
</Flex>
)
}
61 changes: 22 additions & 39 deletions packages/mobile/src/screens/chat-screen/ChatScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isEarliestUnread,
chatCanFetchMoreMessages
} from '@audius/common/utils'
import type { ChatBlast } from '@audius/sdk'
import { Portal } from '@gorhom/portal'
import { useKeyboard } from '@react-native-community/hooks'
import { useFocusEffect } from '@react-navigation/native'
Expand All @@ -38,29 +39,27 @@ import {
HeaderShadow,
KeyboardAvoidingView,
Screen,
ScreenContent,
ProfilePicture
ScreenContent
} from 'app/components/core'
import LoadingSpinner from 'app/components/loading-spinner'
import { PLAY_BAR_HEIGHT } from 'app/components/now-playing-drawer'
import { UserBadges } from 'app/components/user-badges'
import { light } from 'app/haptics'
import { useNavigation } from 'app/hooks/useNavigation'
import { useRoute } from 'app/hooks/useRoute'
import { useToast } from 'app/hooks/useToast'
import { setVisibility } from 'app/store/drawers/slice'
import { makeStyles } from 'app/styles'
import { spacing } from 'app/styles/spacing'
import { useThemePalette } from 'app/utils/theme'

import type { AppTabScreenParamList } from '../app-screen'

import { ChatBlastHeader } from './ChatBlastHeader'
import { ChatBlastSubHeader } from './ChatBlastSubHeader'
import { ChatMessageListItem } from './ChatMessageListItem'
import { ChatMessageSeparator } from './ChatMessageSeparator'
import { ChatTextInput } from './ChatTextInput'
import { ChatUnavailable } from './ChatUnavailable'
import { EmptyChatMessages } from './EmptyChatMessages'
import { ReactionPopup } from './ReactionPopup'
import { UserChatHeader } from './UserChatHeader'
import {
END_REACHED_MIN_MESSAGES,
NEW_MESSAGE_TOAST_SCROLL_THRESHOLD,
Expand Down Expand Up @@ -114,11 +113,6 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({
flexGrow: 1,
paddingHorizontal: spacing(6)
},
profileTitle: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
composeView: {
paddingVertical: spacing(2),
paddingHorizontal: spacing(4),
Expand Down Expand Up @@ -152,11 +146,6 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({
height: spacing(7),
fill: palette.primary
},
userBadgeTitle: {
fontSize: typography.fontSize.medium,
fontFamily: typography.fontByWeight.bold,
color: palette.neutral
},
loadingSpinnerContainer: {
display: 'flex',
flexGrow: 1,
Expand All @@ -170,6 +159,11 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({
emptyContainer: {
display: 'flex',
flexGrow: 1
},
userBadgeTitle: {
fontSize: typography.fontSize.medium,
fontFamily: typography.fontByWeight.bold,
color: palette.neutral
}
}))

Expand Down Expand Up @@ -237,7 +231,6 @@ export const ChatScreen = () => {
const palette = useThemePalette()
const dispatch = useDispatch()
const { toast } = useToast()
const navigation = useNavigation<AppTabScreenParamList>()

const { params } = useRoute<'Chat'>()
const { chatId, presetMessage } = params
Expand All @@ -262,6 +255,10 @@ export const ChatScreen = () => {
const userId = useSelector(getUserId)
const userIdEncoded = encodeHashId(userId)
const chat = useSelector((state) => getChat(state, chatId ?? ''))
const { is_blast: isBlast } = chat ?? {}
// Need additional bottom padding for composer for chat blasts
// because there is an extra screen header.
const chatBlastWithContentOffset = isBlast ? spacing(6) : 0
const chatMessages = useSelector((state) =>
getChatMessages(state, chatId ?? '')
)
Expand Down Expand Up @@ -473,11 +470,11 @@ export const ChatScreen = () => {
[canSendMessage, chat?.is_blast, dispatch]
)

const topBarRight = (
const topBarRight = !isBlast ? (
<TouchableOpacity onPress={handleTopRightPress} hitSlop={spacing(2)}>
<IconKebabHorizontal fill={palette.neutralLight4} />
</TouchableOpacity>
)
) : null

// When reaction popup opens, hide reaction here so it doesn't
// appear underneath the reaction of the message clone inside the
Expand Down Expand Up @@ -566,32 +563,17 @@ export const ChatScreen = () => {
<Screen
url={url}
headerTitle={
otherUser
? () => (
<TouchableOpacity
onPress={() =>
navigation.push('Profile', { id: otherUser.user_id })
}
style={styles.profileTitle}
>
<ProfilePicture
userId={otherUser.user_id}
size='small'
mr='s'
strokeWidth='thin'
/>
<UserBadges
user={otherUser}
nameStyle={styles.userBadgeTitle}
/>
</TouchableOpacity>
)
isBlast
? () => <ChatBlastHeader chat={chat as ChatBlast} />
: otherUser
? () => <UserChatHeader user={otherUser} />
: () => <Text style={styles.userBadgeTitle}>{messages.title}</Text>
}
icon={otherUser ? undefined : IconMessage}
topbarRight={topBarRight}
>
<ScreenContent>
{isBlast ? <ChatBlastSubHeader chat={chat as ChatBlast} /> : null}
<HeaderShadow />
{/* Everything inside the portal displays on top of all other screen contents. */}
<Portal hostName='ChatReactionsPortal'>
Expand Down Expand Up @@ -682,6 +664,7 @@ export const ChatScreen = () => {
>
<View style={styles.whiteBackground} />
<ChatTextInput
extraOffset={chatBlastWithContentOffset}
chatId={chatId}
presetMessage={presetMessage}
onMessageSent={handleMessageSent}
Expand Down
16 changes: 15 additions & 1 deletion packages/mobile/src/screens/chat-screen/ChatTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@ const useStyles = makeStyles(({ spacing, palette, typography }) => ({

type ChatTextInputProps = {
chatId: string
extraOffset?: number // Additional padding needed if screen header size changes
presetMessage?: string
onMessageSent: () => void
}

export const ChatTextInput = ({
chatId,
extraOffset = 0,
presetMessage,
onMessageSent
}: ChatTextInputProps) => {
Expand Down Expand Up @@ -233,6 +235,18 @@ export const ChatTextInput = ({
return parts
}

// For iOS: default padding + extra padding
// For Android: extra padding is slightly larger than iOS, and only
// needed if the screen header size changes
const offset =
Platform.OS === 'ios'
? spacing(1.5) + extraOffset
: Platform.OS === 'android'
? extraOffset
? spacing(1.5) + extraOffset
: undefined
: undefined

return (
<Flex>
{trackId ? (
Expand All @@ -244,7 +258,7 @@ export const ChatTextInput = ({
style={{
position: 'relative',
maxHeight: hasCurrentlyPlayingTrack ? spacing(70) : spacing(80),
paddingBottom: Platform.OS === 'ios' ? spacing(1.5) : undefined
paddingBottom: offset
}}
>
<View
Expand Down
41 changes: 41 additions & 0 deletions packages/mobile/src/screens/chat-screen/UserChatHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { User } from '@audius/common/models'
import { TouchableOpacity } from 'react-native'

import { ProfilePicture } from 'app/components/core'
import UserBadges from 'app/components/user-badges'
import { useNavigation } from 'app/hooks/useNavigation'
import { makeStyles } from 'app/styles'

import type { AppTabScreenParamList } from '../app-screen'

const useStyles = makeStyles(({ spacing, palette, typography }) => ({
profileTitle: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
userBadgeTitle: {
fontSize: typography.fontSize.medium,
fontFamily: typography.fontByWeight.bold,
color: palette.neutral
}
}))

export const UserChatHeader = ({ user }: { user: User }) => {
const styles = useStyles()
const navigation = useNavigation<AppTabScreenParamList>()
return (
<TouchableOpacity
onPress={() => navigation.push('Profile', { id: user.user_id })}
style={styles.profileTitle}
>
<ProfilePicture
userId={user.user_id}
size='small'
mr='s'
strokeWidth='thin'
/>
<UserBadges user={user} nameStyle={styles.userBadgeTitle} />
</TouchableOpacity>
)
}