Skip to content

Commit

Permalink
2916: Fix various issues
Browse files Browse the repository at this point in the history
  • Loading branch information
steffenkleinle committed Jan 30, 2025
1 parent e81db42 commit 912a403
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 70 deletions.
15 changes: 2 additions & 13 deletions native/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const Header = ({
// Save route/canGoBack to state to prevent it from changing during navigating which would lead to flickering of the title and back button
const [previousRoute] = useState(navigation.getState().routes[navigation.getState().routes.length - 2])
const [canGoBack] = useState(navigation.canGoBack())
const { enabled: isTtsEnabled, setVisible: setTtsPlayerVisible, canRead } = useTtsPlayer()
const { enabled: isTtsEnabled, showTtsPlayer } = useTtsPlayer()

const onShare = async () => {
if (!shareUrl) {
Expand Down Expand Up @@ -189,25 +189,14 @@ const Header = ({
renderItem(HeaderButtonTitle.Language, 'language', showItems, goToLanguageChange),
]

const openTtsPlayer = isTtsEnabled
? [
renderOverflowItem(t(HeaderButtonTitle.ReadAloud), () => {
setTtsPlayerVisible(canRead)
if (!canRead) {
showSnackbar({ text: t('nothingToReadFullMessage') })
}
}),
]
: []

const overflowItems = showOverflowItems
? [
...(shareUrl ? [renderOverflowItem(HeaderButtonTitle.Share, onShare)] : []),
...(!buildConfig().featureFlags.fixedCity
? [renderOverflowItem(HeaderButtonTitle.Location, () => navigation.navigate(LANDING_ROUTE))]
: []),
renderOverflowItem(HeaderButtonTitle.Settings, () => navigation.navigate(SETTINGS_ROUTE)),
...openTtsPlayer,
...(isTtsEnabled ? [renderOverflowItem(t(HeaderButtonTitle.ReadAloud), showTtsPlayer)] : []),
...(route.name !== NEWS_ROUTE ? [renderOverflowItem(HeaderButtonTitle.Feedback, navigateToFeedback)] : []),
...(route.name !== DISCLAIMER_ROUTE
? [renderOverflowItem(HeaderButtonTitle.Disclaimer, () => navigation.navigate(DISCLAIMER_ROUTE))]
Expand Down
92 changes: 52 additions & 40 deletions native/src/components/TtsContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { createContext, ReactElement, useCallback, useContext, useEffect, useMemo, useState } from 'react'
import React, { createContext, ReactElement, useCallback, useContext, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AppState } from 'react-native'
import Tts, { Options } from 'react-native-tts'

import { truncate } from 'shared/utils/getExcerpt'

import buildConfig from '../constants/buildConfig'
import { AppContext } from '../contexts/AppContextProvider'
import useAppStateListener from '../hooks/useAppStateListener'
import useSnackbar from '../hooks/useSnackbar'
import { reportError } from '../utils/sentry'
import TtsPlayer from './TtsPlayer'

Expand All @@ -26,18 +26,16 @@ const TTS_OPTIONS: Options = {

export type TtsContextType = {
enabled: boolean
canRead: boolean
visible: boolean
setVisible: (visible: boolean) => void
showTtsPlayer: () => void
sentences: string[] | null
setSentences: (sentences: string[]) => void
}

export const TtsContext = createContext<TtsContextType>({
enabled: false,
canRead: false,
visible: false,
setVisible: () => undefined,
showTtsPlayer: () => undefined,
sentences: [],
setSentences: () => undefined,
})
Expand All @@ -53,35 +51,41 @@ const TtsContainer = ({ children }: TtsContainerProps): ReactElement => {
const [sentences, setSentences] = useState<string[]>([])
const { languageCode } = useContext(AppContext)
const { t } = useTranslation('layout')
const showSnackbar = useSnackbar()
const title = sentences[0] || t('nothingToRead')
const longTitle = truncate(title, { maxChars: MAX_TITLE_DISPLAY_CHARS })
const enabled = buildConfig().featureFlags.tts && !TTS_UNSUPPORTED_LANGUAGES.includes(languageCode)
const canRead = enabled && sentences.length > 0

const initializeTts = useCallback((): void => {
Tts.getInitStatus()
.then(() => Tts.setDefaultLanguage(languageCode))
.catch(async error => {
if (error.code === 'no_engine') {
await Tts.requestInstallEngine().catch((e: string) => reportError(`Failed to install tts engine: : ${e}`))
} else {
reportError(`Tts-Error: ${error.code}`)
}

const initializeTts = useCallback(async (): Promise<void> => {
await Tts.getInitStatus().catch(async error =>
error.code === 'no_engine' ? Tts.requestInstallEngine() : undefined,
)
}, [])

const showTtsPlayer = useCallback((): void => {
if (!enabled || visible) {
return
}
if (sentences.length === 0) {
showSnackbar({ text: t('nothingToReadFullMessage') })
return
}
initializeTts()
.then(() => setVisible(true))
.catch(error => {
reportError(error)
showSnackbar({ text: t('error:unknownError') })
})
}, [languageCode])
}, [initializeTts, enabled, sentences.length, visible, showSnackbar, t])

const stop = useCallback(async (resetSentenceIndex = true) => {
const stopPlayer = useCallback(async () => {
// iOS wrongly sends tts-finish instead of tts-cancel if calling Tts.stop()
// We therefore have to remove the listener before stopping to avoid playing the next sentence
// https://github.com/ak1394/react-native-tts/issues/198
Tts.removeAllListeners('tts-finish')
// Add a listener doing nothing to avoid warnings about unhandled events
Tts.addEventListener('tts-finish', () => undefined)
await Tts.stop()
setIsPlaying(false)
if (resetSentenceIndex) {
setSentenceIndex(0)
}
// The tts-finish event is only fired some time after stop is finished
// We therefore need to wait some time before adding the listener again
await new Promise(resolve => {
Expand All @@ -90,55 +94,63 @@ const TtsContainer = ({ children }: TtsContainerProps): ReactElement => {
})
}, [])

const stop = useCallback(() => {
stopPlayer().catch(reportError)
setIsPlaying(false)
setSentenceIndex(0)
}, [stopPlayer])

const pause = () => {
stopPlayer().catch(reportError)
setIsPlaying(false)
}

const play = useCallback(
async (index = sentenceIndex) => {
await stop()
const sentence = sentences[index]
const safeIndex = Math.max(0, index)
const sentence = sentences[safeIndex]
if (sentence) {
await stopPlayer()
setIsPlaying(true)
setSentenceIndex(index)
Tts.addEventListener('tts-finish', () => play(index + 1))
Tts.addEventListener('tts-finish', () => play(safeIndex + 1))
Tts.setDefaultLanguage(languageCode)
Tts.speak(sentence, TTS_OPTIONS)
} else {
stop()
}
},
[stop, sentenceIndex, sentences],
[stop, stopPlayer, sentenceIndex, sentences, languageCode],
)

useEffect(() => {
if (visible) {
initializeTts()
}
}, [visible, initializeTts])

useAppStateListener(appState => {
if (appState === 'inactive' || appState === 'background') {
stop().catch(reportError)
stop()
}
})

const close = async () => {
setVisible(false)
stop().catch(reportError)
stop()
}

const updateSentences = useCallback(
(newSentences: string[]) => {
setSentences(newSentences)
stop().catch(reportError)
stop()
},
[stop],
)

const ttsContextValue = useMemo(
() => ({
enabled,
canRead,
visible,
setVisible,
showTtsPlayer,
sentences,
setSentences: updateSentences,
}),
[enabled, canRead, visible, sentences, updateSentences],
[enabled, visible, sentences, updateSentences, showTtsPlayer],
)

return (
Expand All @@ -151,7 +163,7 @@ const TtsContainer = ({ children }: TtsContainerProps): ReactElement => {
playPrevious={() => play(sentenceIndex - 1)}
playNext={() => play(sentenceIndex + 1)}

Check notice on line 164 in native/src/components/TtsContainer.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

ℹ Getting worse: Complex Method

TtsContainer increases in cyclomatic complexity from 14 to 15, threshold = 10. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
close={close}
pause={() => stop(false)}
pause={pause}
play={play}
title={longTitle}
/>
Expand Down
3 changes: 2 additions & 1 deletion native/src/components/TtsPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ const StyledTtsPlayer = styled.View<{ $isPlaying: boolean }>`
align-self: center;
position: absolute;
bottom: 5px;
min-height: 93px;
height: 100px;
gap: ${props => (props.$isPlaying ? '0px;' : '20px')};
margin-bottom: 24px;
`

const verticalMargin = 11
Expand Down
6 changes: 3 additions & 3 deletions native/src/components/__tests__/TtsContainer.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ const dummyPage = new PageModel({
})
describe('TtsContainer', () => {
const TestChild = () => {
const { setVisible } = useTtsPlayer(dummyPage)
const { showTtsPlayer } = useTtsPlayer(dummyPage)
useEffect(() => {
setVisible(true)
}, [setVisible])
showTtsPlayer()
}, [showTtsPlayer])
return null
}

Expand Down
20 changes: 7 additions & 13 deletions native/src/hooks/useTtsPlayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ import { AppContext } from '../contexts/AppContextProvider'

const useTtsPlayer = (model?: PageModel | LocalNewsModel | TunewsModel | undefined): TtsContextType => {
const { languageCode } = useContext(AppContext)
const { setSentences, visible, setVisible, enabled, canRead } = useContext(TtsContext)
const ttsContext = useContext(TtsContext)
const { setSentences } = ttsContext

const sentences = useMemo(() => {
if (model) {
const content = parseHTML(model.content)
return [model.title, ...segment(languageCode, content)]
const sentences = segment(languageCode, content).filter((sentence: string) => sentence.length > 0)
return [model.title, ...sentences]
}

return []
Expand All @@ -23,19 +26,10 @@ const useTtsPlayer = (model?: PageModel | LocalNewsModel | TunewsModel | undefin
if (sentences.length) {
setSentences(sentences)
}
return () => {
setSentences([])
}
return () => setSentences([])
}, [sentences, setSentences])

return {
enabled,
canRead,
visible,
setVisible,
sentences,
setSentences,
}
return ttsContext
}

export default useTtsPlayer

0 comments on commit 912a403

Please sign in to comment.