diff --git a/packages/mobile/src/components/comments/CommentDrawerForm.tsx b/packages/mobile/src/components/comments/CommentDrawerForm.tsx
index ab6351cd4fd..e9d46f05430 100644
--- a/packages/mobile/src/components/comments/CommentDrawerForm.tsx
+++ b/packages/mobile/src/components/comments/CommentDrawerForm.tsx
@@ -7,7 +7,6 @@ import {
} from '@audius/common/context'
import type { ID } from '@audius/common/models'
import { Status } from '@audius/common/models'
-import { BottomSheetTextInput } from '@gorhom/bottom-sheet'
import { Box } from '@audius/harmony-native'
@@ -31,11 +30,7 @@ export const CommentDrawerForm = () => {
return (
-
+
)
}
diff --git a/packages/mobile/src/components/comments/CommentForm.tsx b/packages/mobile/src/components/comments/CommentForm.tsx
index 17a5e6c19bb..b7e6d7dc944 100644
--- a/packages/mobile/src/components/comments/CommentForm.tsx
+++ b/packages/mobile/src/components/comments/CommentForm.tsx
@@ -1,38 +1,26 @@
-import { useEffect, useRef } from 'react'
+import { useEffect, useRef, useState } from 'react'
import { useGetUserById } from '@audius/common/api'
import { useCurrentCommentSection } from '@audius/common/context'
import { commentsMessages as messages } from '@audius/common/messages'
import type { ID } from '@audius/common/models'
-import type { FormikHelpers } from 'formik'
-import { Formik, useFormikContext } from 'formik'
import type { TextInput as RNTextInput } from 'react-native'
-import { Flex, IconButton, IconSend } from '@audius/harmony-native'
+import { Box, Flex } from '@audius/harmony-native'
+import { ComposerInput } from '../composer-input'
import { ProfilePicture } from '../core'
-import { HarmonyTextField } from '../fields'
-import LoadingSpinner from '../loading-spinner'
-
-type CommentFormValues = {
- commentMessage: string
- mentions?: ID[]
-}
type CommentFormProps = {
onSubmit: (commentMessage: string, mentions?: ID[]) => void
initialValue?: string
isLoading?: boolean
- TextInputComponent?: typeof RNTextInput
}
-type CommentFormContentProps = Omit<
- CommentFormProps,
- 'onSubmit' | 'initialValue'
->
-
-const CommentFormContent = (props: CommentFormContentProps) => {
- const { isLoading, TextInputComponent } = props
+export const CommentForm = (props: CommentFormProps) => {
+ const { isLoading, onSubmit, initialValue } = props
+ const [messageId, setMessageId] = useState(0)
+ const [initialMessage, setInitialMessage] = useState(initialValue)
const { currentUserId, comments, replyingToComment, editingComment } =
useCurrentCommentSection()
const ref = useRef(null)
@@ -45,30 +33,35 @@ const CommentFormContent = (props: CommentFormContentProps) => {
{ disabled: !replyingToComment }
)
- const { setFieldValue } = useFormikContext()
- const { submitForm } = useFormikContext()
+ // TODO: Add mentions back here
+ const handleSubmit = (message: string) => {
+ onSubmit(message)
+ setMessageId((id) => ++id)
+ }
/**
* Populate and focus input when replying to a comment
*/
useEffect(() => {
if (replyingToComment && replyingToUser) {
- setFieldValue('commentMessage', `@${replyingToUser.handle} `)
+ setInitialMessage(`@${replyingToUser.handle} `)
ref.current?.focus()
}
- }, [replyingToComment, replyingToUser, setFieldValue])
+ }, [replyingToComment, replyingToUser])
/**
* Populate and focus input when editing a comment
*/
useEffect(() => {
if (editingComment) {
- setFieldValue('commentMessage', editingComment.message)
+ setInitialMessage(editingComment.message)
ref.current?.focus()
}
- }, [editingComment, setFieldValue])
+ }, [editingComment])
- const message = comments?.length ? messages.addComment : messages.firstComment
+ const placeholder = comments?.length
+ ? messages.addComment
+ : messages.firstComment
return (
@@ -78,46 +71,15 @@ const CommentFormContent = (props: CommentFormContentProps) => {
style={{ width: 40, height: 40, flexShrink: 0 }}
/>
) : null}
-
- ) : (
-
- )
- }
- TextInputComponent={TextInputComponent}
- />
+
+
+
)
}
-
-export const CommentForm = (props: CommentFormProps) => {
- const { onSubmit, initialValue = '', ...rest } = props
- const handleSubmit = (
- { commentMessage, mentions }: CommentFormValues,
- { resetForm }: FormikHelpers
- ) => {
- onSubmit(commentMessage, mentions)
- resetForm()
- }
-
- const formInitialValues: CommentFormValues = { commentMessage: initialValue }
-
- return (
-
-
-
- )
-}
diff --git a/packages/mobile/src/components/composer-input/ComposerInput.tsx b/packages/mobile/src/components/composer-input/ComposerInput.tsx
new file mode 100644
index 00000000000..c85fa0118d1
--- /dev/null
+++ b/packages/mobile/src/components/composer-input/ComposerInput.tsx
@@ -0,0 +1,285 @@
+import { useCallback, useEffect, useRef, useState } from 'react'
+
+import { useAudiusLinkResolver } from '@audius/common/hooks'
+import { splitOnNewline } from '@audius/common/utils'
+import { Platform, TouchableOpacity, View } from 'react-native'
+import type {
+ NativeSyntheticEvent,
+ TextInputKeyPressEventData,
+ TextInputSelectionChangeEventData
+} from 'react-native/types'
+
+import { Flex, IconSend } from '@audius/harmony-native'
+import { Text, TextInput } from 'app/components/core'
+import { env } from 'app/env'
+import { audiusSdk } from 'app/services/sdk/audius-sdk'
+import { makeStyles } from 'app/styles'
+import { spacing } from 'app/styles/spacing'
+import { useThemeColors } from 'app/utils/theme'
+
+import LoadingSpinner from '../loading-spinner/LoadingSpinner'
+
+import type { ComposerInputProps } from './types'
+
+const BACKSPACE_KEY = 'Backspace'
+
+const messages = {
+ sendMessage: 'Send Message',
+ sendMessagePlaceholder: 'Start typing...'
+}
+
+const useStyles = makeStyles(({ spacing, palette, typography }) => ({
+ composeTextContainer: {
+ display: 'flex',
+ alignItems: 'center',
+ backgroundColor: 'transparent',
+ paddingLeft: spacing(4),
+ paddingVertical: spacing(2),
+ borderRadius: spacing(1)
+ },
+ composeTextInput: {
+ fontSize: typography.fontSize.medium,
+ lineHeight: spacing(6),
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingTop: 0,
+ color: 'transparent'
+ },
+ overlayTextContainer: {
+ position: 'absolute',
+ right: spacing(10),
+ left: 0,
+ zIndex: 0,
+ paddingLeft: spacing(4) + 1,
+ paddingVertical: spacing(3) - 1
+ },
+ overlayText: {
+ fontSize: typography.fontSize.medium,
+ lineHeight: spacing(6),
+ justifyContent: 'center',
+ alignItems: 'center',
+ color: palette.neutral,
+ paddingTop: 0
+ },
+ submit: {
+ paddingRight: spacing(1)
+ },
+ icon: {
+ width: spacing(6),
+ height: spacing(6)
+ }
+}))
+
+export const ComposerInput = (props: ComposerInputProps) => {
+ const styles = useStyles()
+ const {
+ onSubmit,
+ onChange,
+ isLoading,
+ messageId,
+ placeholder,
+ presetMessage,
+ extraOffset = 0
+ } = props
+ const [value, setValue] = useState(presetMessage ?? '')
+ const [selection, setSelection] = useState<{ start: number; end: number }>()
+ const { primary, neutralLight7 } = useThemeColors()
+ const hasLength = value.length > 0
+ const messageIdRef = useRef(messageId)
+
+ const {
+ linkEntities,
+ resolveLinks,
+ restoreLinks,
+ getMatches,
+ handleBackspace
+ } = useAudiusLinkResolver({
+ value,
+ hostname: env.PUBLIC_HOSTNAME,
+ audiusSdk
+ })
+
+ useEffect(() => {
+ const fn = async () => {
+ if (presetMessage) {
+ const editedValue = await resolveLinks(presetMessage)
+ setValue(editedValue)
+ }
+ }
+ fn()
+ }, [presetMessage, resolveLinks])
+
+ useEffect(() => {
+ onChange?.(restoreLinks(value), linkEntities)
+ }, [linkEntities, onChange, restoreLinks, value])
+
+ useEffect(() => {
+ if (messageId !== messageIdRef.current) {
+ messageIdRef.current = messageId
+ setValue('')
+ }
+ }, [messageId])
+
+ const handleChange = useCallback(
+ async (value: string) => {
+ setValue(value)
+ const editedValue = await resolveLinks(value)
+ setValue(editedValue)
+ },
+ [resolveLinks]
+ )
+
+ const handleSelectionChange = useCallback(
+ (e: NativeSyntheticEvent) => {
+ setSelection(e.nativeEvent.selection)
+ },
+ [setSelection]
+ )
+
+ const handleSubmit = useCallback(() => {
+ onSubmit?.(restoreLinks(value), linkEntities)
+ }, [linkEntities, onSubmit, restoreLinks, value])
+
+ const handleKeyDown = useCallback(
+ (e: NativeSyntheticEvent) => {
+ if (e.nativeEvent.key === BACKSPACE_KEY && selection) {
+ const textBeforeCursor = value.slice(0, selection.start)
+ const cursorPosition = selection.start
+ const { editValue } = handleBackspace({
+ cursorPosition,
+ textBeforeCursor
+ })
+
+ if (editValue) {
+ // Delay the deleting of the matched word because
+ // in react native text change will fire on backspace anyway
+ // and we want the handleChange callback to run first.
+ // This is a bit of a hack. In search of a better way!
+ setTimeout(() => {
+ setValue(editValue)
+ }, 100)
+ }
+ }
+ },
+ [handleBackspace, selection, value]
+ )
+
+ const renderIcon = () => (
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ )
+
+ const renderTextDisplay = (value: string) => {
+ const matches = getMatches(value)
+ if (!matches) {
+ const text = splitOnNewline(value)
+ return text.map((t, i) => (
+ {`${t === '\n' ? '\n\n' : t}`}
+ ))
+ }
+ const parts: JSX.Element[] = []
+ let lastIndex = 0
+ for (const match of matches) {
+ const { index, text } = match
+ if (index === undefined) {
+ continue
+ }
+
+ if (index > lastIndex) {
+ // Add text before the match
+ const text = splitOnNewline(value.slice(lastIndex, index))
+ parts.push(
+ ...text.map((t, i) => (
+ {`${t === '\n' ? '\n\n' : t}`}
+ ))
+ )
+ }
+ // Add the matched word with accent color
+ parts.push(
+
+ {text}
+
+ )
+
+ // Update lastIndex to the end of the current match
+ lastIndex = index + text.length
+ }
+
+ // Add remaining text after the last match
+ if (lastIndex < value.length) {
+ const text = splitOnNewline(value.slice(lastIndex))
+ parts.push(
+ ...text.map((t, i) => {`${t}\n`})
+ )
+ }
+
+ 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 (
+
+
+ {renderTextDisplay(value)}
+
+
+
+ )
+}
diff --git a/packages/mobile/src/components/composer-input/index.ts b/packages/mobile/src/components/composer-input/index.ts
new file mode 100644
index 00000000000..ed5a84ec81a
--- /dev/null
+++ b/packages/mobile/src/components/composer-input/index.ts
@@ -0,0 +1,2 @@
+export { ComposerInput } from './ComposerInput'
+export * from './types'
diff --git a/packages/mobile/src/components/composer-input/types.ts b/packages/mobile/src/components/composer-input/types.ts
new file mode 100644
index 00000000000..012ca8279c6
--- /dev/null
+++ b/packages/mobile/src/components/composer-input/types.ts
@@ -0,0 +1,19 @@
+import type { LinkEntity } from '@audius/common/hooks'
+import type { ID } from '@audius/common/models'
+import type { EntityType } from '@audius/sdk'
+
+import type { TextInputProps } from '../core'
+
+export type ComposerInputProps = {
+ messageId: number
+ extraOffset?: number // Additional padding needed if screen header size changes
+ entityId?: ID
+ entityType?: EntityType
+ onChange?: (value: string, linkEntities: LinkEntity[]) => void
+ onSubmit?: (value: string, linkEntities: LinkEntity[]) => void
+ presetMessage?: string
+ isLoading?: boolean
+} & Pick<
+ TextInputProps,
+ 'maxLength' | 'placeholder' | 'onPressIn' | 'readOnly' | 'id'
+>
diff --git a/packages/mobile/src/screens/chat-screen/ChatTextInput.tsx b/packages/mobile/src/screens/chat-screen/ChatTextInput.tsx
index 42ce28dd4ea..6434eb1521e 100644
--- a/packages/mobile/src/screens/chat-screen/ChatTextInput.tsx
+++ b/packages/mobile/src/screens/chat-screen/ChatTextInput.tsx
@@ -1,77 +1,22 @@
-import { useCallback, useEffect, useMemo, useState } from 'react'
+import { useCallback, useState } from 'react'
-import { useAudiusLinkResolver } from '@audius/common/hooks'
-import { chatActions, playerSelectors } from '@audius/common/store'
-import { decodeHashId, splitOnNewline } from '@audius/common/utils'
-import { Platform, TouchableOpacity, View } from 'react-native'
-import type {
- NativeSyntheticEvent,
- TextInputKeyPressEventData,
- TextInputSelectionChangeEventData
-} from 'react-native'
-import { useDispatch, useSelector } from 'react-redux'
+import type { ID } from '@audius/common/models'
+import { chatActions } from '@audius/common/store'
+import type { Nullable } from '@audius/common/utils'
+import { decodeHashId } from '@audius/common/utils'
+import { useDispatch } from 'react-redux'
-import { Flex, IconSend } from '@audius/harmony-native'
-import { Text, TextInput } from 'app/components/core'
-import { env } from 'app/env'
-import { audiusSdk } from 'app/services/sdk/audius-sdk'
-import { makeStyles } from 'app/styles'
-import { spacing } from 'app/styles/spacing'
-import { useThemeColors } from 'app/utils/theme'
+import { Flex } from '@audius/harmony-native'
+import { ComposerInput } from 'app/components/composer-input'
import { ComposerCollectionInfo, ComposerTrackInfo } from './ComposePreviewInfo'
const { sendMessage } = chatActions
-const { getHasTrack } = playerSelectors
const messages = {
startNewMessage: ' Start typing...'
}
-const BACKSPACE_KEY = 'Backspace'
-
-const useStyles = makeStyles(({ spacing, palette, typography }) => ({
- composeTextContainer: {
- display: 'flex',
- alignItems: 'center',
- backgroundColor: 'transparent',
- paddingLeft: spacing(4),
- paddingVertical: spacing(2),
- borderRadius: spacing(1)
- },
- composeTextInput: {
- fontSize: typography.fontSize.medium,
- lineHeight: spacing(6),
- justifyContent: 'center',
- alignItems: 'center',
- paddingTop: 0,
- color: 'transparent'
- },
- overlayTextContainer: {
- position: 'absolute',
- right: spacing(10),
- left: 0,
- zIndex: 0,
- paddingLeft: spacing(4) + 1,
- paddingVertical: spacing(3) - 1
- },
- overlayText: {
- fontSize: typography.fontSize.medium,
- lineHeight: spacing(6),
- justifyContent: 'center',
- alignItems: 'center',
- color: palette.neutral,
- paddingTop: 0
- },
- submit: {
- paddingRight: spacing(1)
- },
- icon: {
- width: spacing(6),
- height: spacing(6)
- }
-}))
-
type ChatTextInputProps = {
chatId: string
extraOffset?: number // Additional padding needed if screen header size changes
@@ -85,167 +30,34 @@ export const ChatTextInput = ({
presetMessage,
onMessageSent
}: ChatTextInputProps) => {
- const styles = useStyles()
const dispatch = useDispatch()
- const { primary, neutralLight7 } = useThemeColors()
-
- const [inputMessage, setInputMessage] = useState(presetMessage ?? '')
- const hasLength = inputMessage.length > 0
- const hasCurrentlyPlayingTrack = useSelector(getHasTrack)
- const [selection, setSelection] = useState<{ start: number; end: number }>()
-
- const {
- linkEntities,
- resolveLinks,
- restoreLinks,
- getMatches,
- handleBackspace
- } = useAudiusLinkResolver({
- value: inputMessage,
- hostname: env.PUBLIC_HOSTNAME,
- audiusSdk
- })
-
- const trackId = useMemo(() => {
- const track = linkEntities.find((e) => e.type === 'track')
- return track ? decodeHashId(track.data.id) : null
- }, [linkEntities])
-
- const collectionId = useMemo(() => {
- const collection = linkEntities.find((e) => e.type === 'collection')
- return collection ? decodeHashId(collection.data.id) : null
- }, [linkEntities])
-
- useEffect(() => {
- const fn = async () => {
- if (presetMessage) {
- const editedValue = await resolveLinks(presetMessage)
- setInputMessage(editedValue)
- }
- }
- fn()
- }, [presetMessage, resolveLinks])
+ const [messageId, setMessageId] = useState(0)
+ const [value, setValue] = useState(presetMessage ?? '')
+ // The track and collection ids used to render the composer preview
+ const [trackId, setTrackId] = useState>(null)
+ const [collectionId, setCollectionId] = useState>(null)
const handleChange = useCallback(
- async (value: string) => {
- setInputMessage(value)
- const editedValue = await resolveLinks(value)
- setInputMessage(editedValue)
- },
- [setInputMessage, resolveLinks]
- )
-
- const handleSelectionChange = useCallback(
- (e: NativeSyntheticEvent) => {
- setSelection(e.nativeEvent.selection)
- },
- [setSelection]
- )
+ // (value: string, linkEntities: LinkEntity[]) => {
+ (value: string, linkEntities: any[]) => {
+ setValue(value)
- const handleKeyDown = useCallback(
- (e: NativeSyntheticEvent) => {
- if (e.nativeEvent.key === BACKSPACE_KEY && selection) {
- const textBeforeCursor = inputMessage.slice(0, selection.start)
- const cursorPosition = selection.start
- const { editValue } = handleBackspace({
- cursorPosition,
- textBeforeCursor
- })
+ const track = linkEntities.find((e) => e.type === 'track')
+ setTrackId(track ? decodeHashId(track.data.id) : null)
- if (editValue) {
- // Delay the deleting of the matched word because
- // in react native text change will fire on backspace anyway
- // and we want the handleChange callback to run first.
- // This is a bit of a hack. In search of a better way!
- setTimeout(() => {
- setInputMessage(editValue)
- }, 100)
- }
- }
+ const collection = linkEntities.find((e) => e.type === 'collection')
+ setCollectionId(collection ? decodeHashId(collection.data.id) : null)
},
- [selection, handleBackspace, setInputMessage, inputMessage]
+ []
)
- const handleSubmit = useCallback(() => {
- if (chatId && inputMessage) {
- dispatch(sendMessage({ chatId, message: restoreLinks(inputMessage) }))
- setInputMessage('')
+ const handleSubmit = useCallback(async () => {
+ if (chatId && value) {
+ dispatch(sendMessage({ chatId, message: value }))
onMessageSent()
+ setMessageId((id) => ++id)
}
- }, [inputMessage, chatId, dispatch, onMessageSent, restoreLinks])
-
- const renderIcon = () => (
-
-
-
- )
-
- const renderChatDisplay = (value: string) => {
- const matches = getMatches(value)
- if (!matches) {
- const text = splitOnNewline(value)
- return text.map((t, i) => (
- {`${t === '\n' ? '\n\n' : t}`}
- ))
- }
- const parts: JSX.Element[] = []
- let lastIndex = 0
- for (const match of matches) {
- const { index, text } = match
- if (index === undefined) {
- continue
- }
-
- if (index > lastIndex) {
- // Add text before the match
- const text = splitOnNewline(value.slice(lastIndex, index))
- parts.push(
- ...text.map((t, i) => (
- {`${t === '\n' ? '\n\n' : t}`}
- ))
- )
- }
- // Add the matched word with accent color
- parts.push(
-
- {text}
-
- )
-
- // Update lastIndex to the end of the current match
- lastIndex = index + text.length
- }
-
- // Add remaining text after the last match
- if (lastIndex < value.length) {
- const text = splitOnNewline(value.slice(lastIndex))
- parts.push(
- ...text.map((t, i) => {`${t}\n`})
- )
- }
-
- 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
+ }, [chatId, dispatch, onMessageSent, value])
return (
@@ -254,47 +66,14 @@ export const ChatTextInput = ({
) : collectionId ? (
) : null}
-
-
-
- {renderChatDisplay(inputMessage)}
-
-
-
-
+
)
}