diff --git a/packages/common/src/hooks/chats/useChatBlastAudienceContent.ts b/packages/common/src/hooks/chats/useChatBlastAudienceContent.ts index 9e224f4db55..d91d884a463 100644 --- a/packages/common/src/hooks/chats/useChatBlastAudienceContent.ts +++ b/packages/common/src/hooks/chats/useChatBlastAudienceContent.ts @@ -12,6 +12,7 @@ import { } from '~/api' import { decodeHashId, + getChatBlastAudienceDescription, getChatBlastCTA, getChatBlastSecondaryTitle, getChatBlastTitle @@ -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 diff --git a/packages/common/src/utils/chatUtils.ts b/packages/common/src/utils/chatUtils.ts index 1f22ed4229a..b1413424a8b 100644 --- a/packages/common/src/utils/chatUtils.ts +++ b/packages/common/src/utils/chatUtils.ts @@ -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', @@ -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 diff --git a/packages/common/src/utils/hashIds.ts b/packages/common/src/utils/hashIds.ts index 9b77d43c94e..0dbcc81e32c 100644 --- a/packages/common/src/utils/hashIds.ts +++ b/packages/common/src/utils/hashIds.ts @@ -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 => { +export const decodeHashId = (id?: string): Nullable => { + if (!id) return null try { const ids = hashids.decode(id) if (!ids.length) return null diff --git a/packages/mobile/src/screens/chat-screen/ChatBlastHeader.tsx b/packages/mobile/src/screens/chat-screen/ChatBlastHeader.tsx new file mode 100644 index 00000000000..e8255ae20f1 --- /dev/null +++ b/packages/mobile/src/screens/chat-screen/ChatBlastHeader.tsx @@ -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})` +} + +export const ChatBlastHeader = ({ chat }: { chat: ChatBlast }) => { + const { chatBlastTitle, audienceCount } = useChatBlastAudienceContent({ + chat + }) + return ( + + + {chatBlastTitle} + {audienceCount ? ( + + {messages.chatBlastTitleAudienceCount(audienceCount)} + + ) : null} + + ) +} diff --git a/packages/mobile/src/screens/chat-screen/ChatBlastSubHeader.tsx b/packages/mobile/src/screens/chat-screen/ChatBlastSubHeader.tsx new file mode 100644 index 00000000000..e8c5d759eed --- /dev/null +++ b/packages/mobile/src/screens/chat-screen/ChatBlastSubHeader.tsx @@ -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 ( + + {decodedId ? ( + + {audienceContentType === 'track' ? ( + + ) : ( + + )} + + {contentTitle} + + + ) : ( + + {chatBlastAudienceDescription} + + )} + + ) +} diff --git a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx index 958e34221b8..2400b7b53ed 100644 --- a/packages/mobile/src/screens/chat-screen/ChatScreen.tsx +++ b/packages/mobile/src/screens/chat-screen/ChatScreen.tsx @@ -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' @@ -38,14 +39,11 @@ 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' @@ -53,14 +51,15 @@ 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, @@ -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), @@ -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, @@ -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 } })) @@ -237,7 +231,6 @@ export const ChatScreen = () => { const palette = useThemePalette() const dispatch = useDispatch() const { toast } = useToast() - const navigation = useNavigation() const { params } = useRoute<'Chat'>() const { chatId, presetMessage } = params @@ -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 ?? '') ) @@ -473,11 +470,11 @@ export const ChatScreen = () => { [canSendMessage, chat?.is_blast, dispatch] ) - const topBarRight = ( + const topBarRight = !isBlast ? ( - ) + ) : null // When reaction popup opens, hide reaction here so it doesn't // appear underneath the reaction of the message clone inside the @@ -566,32 +563,17 @@ export const ChatScreen = () => { ( - - navigation.push('Profile', { id: otherUser.user_id }) - } - style={styles.profileTitle} - > - - - - ) + isBlast + ? () => + : otherUser + ? () => : () => {messages.title} } icon={otherUser ? undefined : IconMessage} topbarRight={topBarRight} > + {isBlast ? : null} {/* Everything inside the portal displays on top of all other screen contents. */} @@ -682,6 +664,7 @@ export const ChatScreen = () => { > ({ 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) => { @@ -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 ( {trackId ? ( @@ -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 }} > ({ + 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() + return ( + navigation.push('Profile', { id: user.user_id })} + style={styles.profileTitle} + > + + + + ) +}