Created
May 17, 2024 07:40
-
-
Save efstathiosntonas/c8ed187dfb19e5474e063c11cfef8adf to your computer and use it in GitHub Desktop.
Audio Player Chat Message with react-native-awesome-slider & react-native-audio-player-recorder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AudioRecorderPlayer, { PlayBackType } from "react-native-audio-recorder-player"; | |
import { isIOS } from "utils/utils"; | |
type Callback = (args: { data?: PlayBackType; status: AudioStatus }) => void; | |
type Path = string | undefined; | |
export enum AudioStatus { | |
PAUSED = "PAUSED", | |
PLAYING = "PLAYING", | |
RESUMED = "RESUMED", | |
STARTED = "STARTED", | |
STOPPED = "STOPPED" | |
} | |
let audioRecorderPlayer: AudioRecorderPlayer | undefined; | |
let currentPath: Path; | |
let currentCallback: Callback = () => {}; | |
let currentPosition = 0; | |
export const startPlayer = async (path: string, callback: Callback) => { | |
if (currentPath === undefined) { | |
currentPath = path; | |
currentCallback = callback; | |
} else if (currentPath !== path) { | |
if (audioRecorderPlayer !== undefined) { | |
await stopPlayer(); | |
} | |
currentPath = path; | |
currentCallback = callback; | |
} | |
if (audioRecorderPlayer === undefined) { | |
audioRecorderPlayer = new AudioRecorderPlayer(); | |
await audioRecorderPlayer.setSubscriptionDuration(0.01); | |
} | |
const shouldBeResumed = currentPath === path && currentPosition > 0; | |
if (shouldBeResumed) { | |
await audioRecorderPlayer.resumePlayer(); | |
currentCallback({ | |
status: AudioStatus.RESUMED | |
}); | |
return; | |
} | |
await audioRecorderPlayer.startPlayer(currentPath); | |
currentCallback({ | |
status: AudioStatus.STARTED | |
}); | |
const tolerance = 100; | |
audioRecorderPlayer.addPlayBackListener(async (e) => { | |
if ( | |
isIOS | |
? e.currentPosition === e.duration || e.currentPosition >= e.duration | |
: e.currentPosition >= e.duration || e.duration - e.currentPosition <= tolerance | |
) { | |
await stopPlayer(); | |
} else { | |
currentPosition = e.currentPosition; | |
currentCallback({ | |
status: AudioStatus.PLAYING, | |
data: e | |
}); | |
} | |
return; | |
}); | |
}; | |
export const seekPlayer = async (ms: number) => { | |
try { | |
if (audioRecorderPlayer) { | |
await audioRecorderPlayer.seekToPlayer(ms); | |
await audioRecorderPlayer.resumePlayer(); | |
currentCallback({ | |
status: AudioStatus.RESUMED, | |
// @ts-ignore | |
data: { currentPosition: ms } | |
}); | |
} | |
} catch (e) { | |
console.log(`error while pausing player during seek, error: ${e}`); | |
} | |
}; | |
export const pausePlayer = async () => { | |
await audioRecorderPlayer?.pausePlayer(); | |
currentCallback({ status: AudioStatus.PAUSED }); | |
}; | |
export const stopPlayer = async () => { | |
await audioRecorderPlayer?.stopPlayer(); | |
audioRecorderPlayer?.removePlayBackListener(); | |
currentPosition = 0; | |
currentPath = undefined; | |
currentCallback({ status: AudioStatus.STOPPED }); | |
audioRecorderPlayer = undefined; | |
}; | |
export const msToHMS = (duration: number) => { | |
let seconds: string | number = Math.floor((duration / 1000) % 60), | |
minutes: string | number = Math.floor((duration / (1000 * 60)) % 60); | |
// hours = hours < 10 ? "0" + hours : hours; | |
minutes = minutes < 10 ? "0" + minutes : minutes; | |
seconds = seconds < 10 ? "0" + seconds : seconds; | |
return `${minutes}:${seconds}`; | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { memo, useCallback, useMemo, useState } from "react"; | |
import { Pressable, View } from "react-native"; | |
import dayjs from "dayjs"; | |
import { TFunction } from "i18next"; | |
import isEqual from "react-fast-compare"; | |
import { Text } from "react-native-fast-text"; | |
import { useSharedValue } from "react-native-reanimated"; | |
import { createStyleSheet, useStyles } from "react-native-unistyles"; | |
import FontAwesome6Pro from "react-native-vector-icons/FontAwesome6Pro"; | |
import LoadingIndicator from "@components/LoadingIndicator"; | |
import { textMedium } from "@utils/fonts"; | |
import fontSize from "@utils/fontSize"; | |
import { hitSlop } from "@utils/hitSlop"; | |
import { breadcrumb, isIOS } from "@utils/utils"; | |
import useStore from "@store/useStore"; | |
import { | |
BorderRadiusType, | |
getBorderRadius, | |
getTopBorderRightRadius | |
} from "../../utils/utils"; | |
import { | |
AudioStatus, | |
msToHMS, | |
pausePlayer, | |
startPlayer, | |
stopPlayer | |
} from "./AudioManager"; | |
import TrackPlayerProgress from "./components/TrackPlayerProgress"; | |
import { Chat_Message } from "@generated/graphql"; | |
const buttonHitSlop = hitSlop(); | |
const MessageAudio = ({ | |
isLeft, | |
isMessageRepliedTo, | |
isTargetedMessage, | |
message, | |
nextAuthorId, | |
prevAuthorId, | |
t, | |
target | |
}: { | |
isLeft: boolean; | |
isMessageRepliedTo: boolean; | |
isTargetedMessage: boolean; | |
message: Chat_Message; | |
nextAuthorId?: string; | |
prevAuthorId?: string; | |
t: TFunction; | |
target?: string; | |
}) => { | |
const { styles, theme } = useStyles(stylesheet); | |
const [playing, setPlaying] = useState<boolean>(false); | |
const [paused, setPaused] = useState<boolean>(false); | |
const [playTime, setPlayTime] = useState<string>("00:00"); | |
const progress = useSharedValue(0); | |
const { | |
setAudioPlaying, | |
constants: { time } | |
} = useStore((store) => ({ | |
setAudioPlaying: store.setAudioPlaying, | |
constants: store.constants | |
})); | |
const createdAt = useMemo( | |
() => dayjs(message.created_at).format(time), | |
[message.created_at, time] | |
); | |
const playingTime = useMemo( | |
() => | |
playTime.length === 8 | |
? playTime.slice(0, -3) | |
: playTime.length === 5 | |
? playTime | |
: "00:00", | |
[playTime] | |
); | |
const messageDuration = useMemo( | |
() => | |
message?.audio?.time.length === 8 | |
? msToHMS(Math.round(message?.audio?.secs / 1000) * 1000) | |
: "00:00", | |
[message?.audio?.secs, message?.audio?.time.length] | |
); | |
const playAudio = useCallback(async () => { | |
await startPlayer(message?.audio?.url, (res: any) => { | |
const { status } = res; | |
//console.log({ status }); | |
switch (status) { | |
case AudioStatus.STARTED: { | |
// console.log("BEGIN AUDIO"); | |
setPlaying(true); | |
setAudioPlaying(true); | |
break; | |
} | |
case AudioStatus.PLAYING: { | |
// console.log("PLAYING AUDIO"); | |
const { currentPosition } = res.data; | |
progress.value = currentPosition; | |
setPlayTime(msToHMS(currentPosition)); | |
break; | |
} | |
case AudioStatus.STOPPED: { | |
progress.value = 0; | |
// console.log("STOP AUDIO"); | |
setPlaying(false); | |
setAudioPlaying(false); | |
setPaused(false); | |
setPlayTime("00:00:00"); | |
break; | |
} | |
case AudioStatus.PAUSED: { | |
// console.log("PAUSE AUDIO"); | |
setPaused(true); | |
setAudioPlaying(false); | |
break; | |
} | |
case AudioStatus.RESUMED: { | |
// console.log("RESUME AUDIO"); | |
setPaused(false); | |
setAudioPlaying(true); | |
break; | |
} | |
default: | |
// console.log("REACHING F DEFAULT?"); | |
stopPlayer().catch((e) => | |
console.log(`error while stopping player on unmount, ${e}`) | |
); | |
setPlaying(false); | |
setAudioPlaying(false); | |
setPaused(false); | |
setPlayTime("00:00:00"); | |
} | |
}); | |
}, [message?.audio?.url, progress, setAudioPlaying]); | |
const pauseAudio = useCallback(async () => { | |
breadcrumb("User Paused Audio Message"); | |
await pausePlayer(); | |
}, []); | |
const onPressPlay = useCallback(() => { | |
playAudio().catch((e) => `error while play ${e}`); | |
}, [playAudio]); | |
const renderPlayButton = useCallback(() => { | |
return ( | |
<Pressable | |
accessibilityHint={t("chat_message.accessibility.buttons.audio_player.play.hint")} | |
accessibilityLabel={t( | |
"chat_message.accessibility.buttons.audio_player.play.label" | |
)} | |
accessibilityRole="button" | |
hitSlop={buttonHitSlop} | |
onPress={onPressPlay} | |
style={({ pressed }) => styles.playPauseButton(pressed)} | |
> | |
<FontAwesome6Pro | |
color={isLeft ? theme.colors.interaction["500"] : theme.colors.neutrals["600"]} | |
name="circle-play" | |
size={fontSize(32)} | |
solid | |
/> | |
</Pressable> | |
); | |
}, [t, onPressPlay, isLeft, theme.colors.interaction, theme.colors.neutrals, styles]); | |
const onPressPause = useCallback( | |
() => pauseAudio().catch((e) => console.log(`error while pause ${e}`)), | |
[pauseAudio] | |
); | |
const renderPauseButton = useCallback(() => { | |
return ( | |
<Pressable | |
accessibilityHint={t( | |
"chat_message.accessibility.buttons.audio_player.pause.hint" | |
)} | |
accessibilityLabel={t( | |
"chat_message.accessibility.buttons.audio_player.pause.label" | |
)} | |
accessibilityRole="button" | |
hitSlop={buttonHitSlop} | |
onPress={onPressPause} | |
style={({ pressed }) => styles.playPauseButton(pressed)} | |
> | |
<FontAwesome6Pro | |
color={isLeft ? theme.colors.interaction["500"] : theme.colors.neutrals["600"]} | |
name="circle-pause" | |
size={fontSize(32)} | |
solid | |
/> | |
</Pressable> | |
); | |
}, [t, onPressPause, isLeft, theme.colors.interaction, theme.colors.neutrals, styles]); | |
const loadingIndicator = useMemo(() => { | |
return playing && progress.value === 0 && !paused ? ( | |
<LoadingIndicator | |
indicatorColor={ | |
isLeft ? theme.colors.secondary["700"] : theme.colors.secondary["500"] | |
} | |
size={fontSize(32)} | |
style={styles.loadingIndicator(isLeft)} | |
/> | |
) : null; | |
}, [isLeft, paused, playing, progress.value, styles, theme.colors.secondary]); | |
return ( | |
<View | |
accessibilityHint="" | |
accessibilityLabel={t( | |
"chat_message.accessibility.chat_message.your_voice_message {{ duration }} {{ sent_to }} {{ createdAt }}", | |
{ | |
duration: messageDuration, | |
sent_to: target, | |
createdAt | |
} | |
)} | |
style={styles.container( | |
isLeft, | |
message.author_id, | |
prevAuthorId, | |
nextAuthorId, | |
isTargetedMessage, | |
isMessageRepliedTo | |
)} | |
> | |
<View style={styles.controls}> | |
<View style={styles.playPauseButtonContainer}> | |
{!playing || paused ? renderPlayButton() : null} | |
{playing && progress.value === 0 && !paused ? loadingIndicator : null} | |
{!paused && playing && progress.value > 0 ? renderPauseButton() : null} | |
</View> | |
<View style={styles.track}> | |
<TrackPlayerProgress | |
disabled={!playing} | |
duration={message?.audio?.secs} | |
isLeft={isLeft} | |
progress={progress} | |
/> | |
</View> | |
{playing ? ( | |
<Text style={styles.messageDuration(isLeft)}>{playingTime}</Text> | |
) : ( | |
<Text style={styles.messageDuration(isLeft)}>{messageDuration}</Text> | |
)} | |
</View> | |
<Text style={styles.createdAt(isLeft)}>{createdAt}</Text> | |
</View> | |
); | |
}; | |
const stylesheet = createStyleSheet((theme) => ({ | |
container: ( | |
isLeft: boolean, | |
authorId: string, | |
prevAuthorId?: string, | |
nextAuthorId?: string, | |
isTargetedMessage?: boolean, | |
isMessageRepliedTo?: boolean | |
) => ({ | |
borderWidth: isTargetedMessage || isMessageRepliedTo ? 2 : 0, | |
borderColor: theme.colors.secondary["500"], | |
backgroundColor: isLeft | |
? theme.colors.secondary["100"] | |
: theme.colors.neutrals["250"], | |
flexDirection: "column", | |
paddingVertical: theme.spacing.mvs16, | |
paddingHorizontal: theme.spacing.s12, | |
width: theme.spacing.s272, | |
borderTopRightRadius: getTopBorderRightRadius( | |
prevAuthorId, | |
authorId, | |
isLeft, | |
theme.spacing | |
), | |
borderBottomRightRadius: getBorderRadius( | |
undefined, | |
nextAuthorId, | |
authorId, | |
isLeft, | |
theme.spacing, | |
BorderRadiusType.BottomRight | |
), | |
borderTopLeftRadius: getBorderRadius( | |
undefined, | |
nextAuthorId, | |
authorId, | |
isLeft, | |
theme.spacing, | |
BorderRadiusType.TopLeft | |
), | |
borderBottomLeftRadius: getBorderRadius( | |
undefined, | |
nextAuthorId, | |
authorId, | |
isLeft, | |
theme.spacing, | |
BorderRadiusType.BottomLeft | |
), | |
marginTop: theme.spacing.mvs2 | |
}), | |
controls: { | |
alignItems: "center", | |
flexDirection: "row", | |
justifyContent: "center" | |
}, | |
createdAt: (isLeft: boolean) => ({ | |
alignSelf: "flex-end", | |
color: isLeft ? theme.colors.secondary["500"] : theme.colors.neutrals["500"], | |
zIndex: 9999, | |
...theme.typography.paragraph.xsmall.medium | |
}), | |
loadingIndicator: (isLeft: boolean) => ({ | |
backgroundColor: isLeft | |
? theme.colors.secondary["500"] | |
: theme.colors.neutrals["300"], | |
borderRadius: theme.spacing.mvs50 | |
}), | |
messageDuration: (isLeft: boolean) => ({ | |
color: isLeft ? theme.colors.secondary["800"] : theme.colors.neutrals["800"], | |
...theme.typography.paragraph.small.semibold, | |
fontFamily: isIOS ? textMedium : "monospace", | |
fontVariant: ["tabular-nums"] | |
}), | |
playPauseButton: (pressed: boolean) => ({ | |
opacity: pressed ? 0.7 : 1 | |
}), | |
playPauseButtonContainer: { | |
height: fontSize(32), | |
width: fontSize(32) | |
}, | |
track: { | |
flex: 1, | |
marginLeft: theme.spacing.s8 | |
} | |
})); | |
export default memo(MessageAudio, isEqual); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, { memo, useCallback, useMemo } from "react"; | |
import { View } from "react-native"; | |
import isEqual from "react-fast-compare"; | |
import { Slider } from "react-native-awesome-slider"; | |
import { SharedValue, useDerivedValue, useSharedValue } from "react-native-reanimated"; | |
import { createStyleSheet, useStyles } from "react-native-unistyles"; | |
import { s } from "@utils/size-matters"; | |
import { breadcrumb } from "@utils/utils"; | |
import { msToHMS, pausePlayer, seekPlayer } from "../AudioManager"; | |
interface IProps { | |
disabled: boolean; | |
duration: number | undefined; | |
isLeft: boolean; | |
progress: SharedValue<number>; | |
} | |
const TrackPlayerProgress = ({ disabled, duration, progress, isLeft }: IProps) => { | |
const { styles, theme } = useStyles(stylesheet); | |
const isScrubbing = useSharedValue(false); | |
const minValue = useSharedValue(0); | |
const maxValue = useDerivedValue(() => { | |
return duration; | |
}, [duration]); | |
const onSlidingStart = useCallback(async () => { | |
breadcrumb("User Seeked and Paused Audio Message"); | |
await pausePlayer(); | |
}, []); | |
const onSlidingComplete = (slideValue: number) => { | |
seekPlayer(slideValue).then(() => { | |
isScrubbing.value = false; | |
}); | |
}; | |
const trackTheme = useMemo( | |
() => ({ | |
minimumTrackTintColor: isLeft | |
? theme.colors.interaction["500"] | |
: theme.colors.neutrals["600"], | |
maximumTrackTintColor: isLeft | |
? theme.colors.secondary["300"] | |
: theme.colors.neutrals["400"], | |
bubbleBackgroundColor: theme.colors.neutrals["500"], | |
bubbleTextColor: theme.colors.neutrals["1100"] | |
}), | |
[isLeft, theme.colors.interaction, theme.colors.neutrals, theme.colors.secondary] | |
); | |
return ( | |
<View style={styles.container}> | |
<View style={styles.sliderInnerContainer}> | |
<Slider | |
bubble={msToHMS} | |
bubbleTranslateY={-22} | |
containerStyle={styles.sliderContainer} | |
disable={disabled} | |
disableTrackFollow | |
isScrubbing={isScrubbing} | |
// @ts-ignore | |
maximumValue={maxValue} | |
minimumValue={minValue} | |
onSlidingComplete={onSlidingComplete} | |
onSlidingStart={onSlidingStart} | |
progress={progress} | |
sliderHeight={theme.spacing.mvs6} | |
theme={trackTheme} | |
thumbWidth={20} | |
/> | |
</View> | |
</View> | |
); | |
}; | |
const stylesheet = createStyleSheet((theme) => ({ | |
container: { | |
alignItems: "center", | |
marginLeft: s(5), | |
marginRight: s(15) | |
}, | |
sliderInnerContainer: { | |
maxWidth: 500, | |
position: "relative", | |
width: "100%" | |
}, | |
sliderContainer: { | |
borderRadius: theme.spacing.mvs20 | |
} | |
})); | |
export default memo(TrackPlayerProgress, isEqual); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment