Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2916: Add tts on android #2950

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0cae129
2916: tts Part1 modifying the base text component
bahaaTuffaha Sep 17, 2024
b92918e
2916: tts for webView Part 2
bahaaTuffaha Sep 18, 2024
98f9372
Improving tts and creating TtsPlayer component
bahaaTuffaha Sep 23, 2024
4e7be39
2916: Adding Tests for TtsPlayer
bahaaTuffaha Sep 24, 2024
6cc4ea9
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Sep 24, 2024
139fcd9
2916: tts creating the UI
bahaaTuffaha Sep 25, 2024
c223cf3
2916: tts UI part 2
bahaaTuffaha Sep 26, 2024
b3ba0d2
2916: improvements to reading and volume changing
bahaaTuffaha Sep 27, 2024
773278e
2916: improvements and code is moved to app level as a wrapper
bahaaTuffaha Sep 30, 2024
ce63e8c
2916: Preventing tts from running on wrong screens
bahaaTuffaha Oct 1, 2024
7203bf3
2916: Added translation and enhancements to next and prev sentence re…
bahaaTuffaha Oct 2, 2024
05954fc
2916: Stopping tts in background
bahaaTuffaha Oct 3, 2024
eb4c7ff
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Oct 3, 2024
ea59a45
2916: Fixing TtsPlayer.spec and adding a mock object
bahaaTuffaha Oct 5, 2024
1630047
2916: Requested changes: fixing SVGs and changing the way it extracts…
bahaaTuffaha Oct 10, 2024
01f91e8
Merge branch 'main' into 2916-Add-TTS-on-android
bahaaTuffaha Oct 11, 2024
c65eab2
2916: updating yarn.lock
bahaaTuffaha Oct 11, 2024
81d0333
2916: Requested changes: fixing crashes and optimizing SVGs
bahaaTuffaha Oct 14, 2024
4b796b7
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Oct 14, 2024
9f09844
2916: Fixing TtsPlayer.spec missing react-native-reanimated mock func…
bahaaTuffaha Oct 14, 2024
b1b9656
2916: Requested Changes: adjusting UI and removing duplicate code
bahaaTuffaha Oct 17, 2024
042e2ac
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Oct 17, 2024
0638620
2916: Adding tts for FeatureFlagsType
bahaaTuffaha Nov 12, 2024
4fa95cf
2916: Requested Changes for splitting logic and using parseHTML
bahaaTuffaha Nov 14, 2024
f7bc038
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Nov 14, 2024
a1146d8
2916: Removed Volume slider for tts
bahaaTuffaha Nov 18, 2024
ebbbca3
2916: Requested Changes for removing unnecessary lines and replacing …
bahaaTuffaha Nov 19, 2024
5b01e5d
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Nov 20, 2024
1261492
2916: Replacing TouchableOpacity with Pressable
bahaaTuffaha Nov 22, 2024
4c068af
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Nov 22, 2024
3ff2af1
2916: Adding more languages to the unsupportedLanguagesForTts list
bahaaTuffaha Nov 27, 2024
2612f48
2916: requested Changes: refactoring TtsContainer
bahaaTuffaha Dec 4, 2024
03a0583
2916: Refactoring part 2
bahaaTuffaha Dec 5, 2024
2513acd
2916: Refactoring part 3
bahaaTuffaha Dec 5, 2024
83c8ec4
Merge remote-tracking branch 'origin' into 2916-Add-TTS-on-android
bahaaTuffaha Dec 5, 2024
117aea8
2916: Removing unnessasery dependency from useTtsPlayer's useEffect
bahaaTuffaha Dec 5, 2024
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
1 change: 1 addition & 0 deletions assets/icons/no-sound.svg
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/pause.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/play.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/playback.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/icons/sound.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions build-configs/BuildConfigType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type FeatureFlagsType = FixedCityType & {
cityNotCooperating?: boolean
cityNotCooperatingTemplate: string | null
chat: boolean
tts: boolean
}

// Available on all platforms
Expand Down
1 change: 1 addition & 0 deletions build-configs/aschaffenburg/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const commonAschaffenburgBuildConfig: CommonBuildConfigType = {
fixedCity: 'hallo',
cityNotCooperatingTemplate: null,
chat: false,
tts: false,
},
aboutUrls: {
default: 'https://www.aschaffenburg.de/halloaschaffenburg',
Expand Down
4 changes: 4 additions & 0 deletions build-configs/common/theme/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type ColorsType = {
negativeHighlight: string
invalidInput: string
linkColor: string
grayBackgroundColor: string
slightlyDarkGray: string
}
export const commonLightColors = {
backgroundAccentColor: '#fafafa',
Expand All @@ -31,4 +33,6 @@ export const commonLightColors = {
negativeHighlight: '#8b0000',
invalidInput: '#B3261E',
linkColor: '#0b57d0',
grayBackgroundColor: '#dedede',
slightlyDarkGray: '#b9b9b9',
}
1 change: 1 addition & 0 deletions build-configs/integreat-e2e/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const integreatE2e = {
fixedCity: null,
cityNotCooperatingTemplate,
chat: false,
tts: false,
},
}
const commonIntegreatE2eBuildConfig: CommonBuildConfigType = {
Expand Down
1 change: 1 addition & 0 deletions build-configs/integreat-test-cms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const integreatTestCms = {
fixedCity: null,
cityNotCooperatingTemplate,
chat: true,
tts: true,
},
}
export const commonIntegreatTestCmsBuildConfig: CommonBuildConfigType = {
Expand Down
1 change: 1 addition & 0 deletions build-configs/integreat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const commonIntegreatBuildConfig: CommonBuildConfigType = {
fixedCity: null,
cityNotCooperatingTemplate,
chat: true,
tts: true,
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved
},
aboutUrls: {
default: 'https://integreat-app.de/about/',
Expand Down
1 change: 1 addition & 0 deletions build-configs/malte-test-cms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const commonMalteTestCmsBuildConfig: CommonBuildConfigType = {
fixedCity: null,
cityNotCooperatingTemplate: null,
chat: false,
tts: false,
},
}

Expand Down
1 change: 1 addition & 0 deletions build-configs/malte/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const commonMalteBuildConfig: CommonBuildConfigType = {
fixedCity: null,
cityNotCooperatingTemplate: null,
chat: false,
tts: false,
},
aboutUrls: {
default: 'https://www.malteser-werke.de/malte-app',
Expand Down
1 change: 1 addition & 0 deletions build-configs/obdach/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const commonObdachBuildConfig: CommonBuildConfigType = {
fixedCity: null,
cityNotCooperatingTemplate: null,
chat: false,
tts: false,
},
aboutUrls: {
default: 'https://tuerantuer.de/digitalfabrik/projekte/netzwerkobdachwohnen/',
Expand Down
2 changes: 2 additions & 0 deletions native/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ jest.mock('react-native-permissions', () => require('react-native-permissions/mo
// https://reactnavigation.org/docs/testing#mocking-native-modules
require('react-native-gesture-handler/jestSetup')

jest.mock('react-native-tts')

jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock')

Expand Down
3 changes: 3 additions & 0 deletions native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@sentry/types": "^8.33.1",
"accordion-collapse-react-native": "^1.1.1",
"build-configs": "0.0.1",
"entities": "^5.0.0",
"htmlparser2": "^9.1.0",
"i18next": "^23.14.0",
"intl-pluralrules": "^2.0.1",
Expand All @@ -87,9 +88,11 @@
"react-native-safe-area-context": "^4.10.9",
"react-native-screens": "^3.34.0",
"react-native-svg": "^15.6.0",
"react-native-tts": "^4.1.1",
"react-native-url-polyfill": "^2.0.0",
"react-navigation-header-buttons": "^11.2.1",
"rrule": "^2.8.1",
"sentencex": "^0.4.2",
"shared": "0.0.1",
"styled-components": "^6.1.13",
"stylis": "^4.3.4",
Expand Down
1 change: 1 addition & 0 deletions native/src/@types/sentencex.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module 'sentencex'
23 changes: 13 additions & 10 deletions native/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import IOSSafeAreaView from './components/IOSSafeAreaView'
import SnackbarContainer from './components/SnackbarContainer'
import StaticServerProvider from './components/StaticServerProvider'
import StatusBar from './components/StatusBar'
import TtsPlayer from './components/TtsPlayer'
import { RoutesParamsType } from './constants/NavigationTypes'
import buildConfig from './constants/buildConfig'
import { userAgent } from './constants/endpoint'
Expand Down Expand Up @@ -87,16 +88,18 @@ const App = (): ReactElement => {
<SafeAreaProvider>
<AppContextProvider>
<SnackbarContainer>
<>
<StatusBar />
<IOSSafeAreaView>
<NavigationContainer onStateChange={onStateChange} linking={linking}>
<HeaderButtonsProvider stackType='native'>
<Navigator />
</HeaderButtonsProvider>
</NavigationContainer>
</IOSSafeAreaView>
</>
<TtsPlayer>
<>
<StatusBar />
<IOSSafeAreaView>
<NavigationContainer onStateChange={onStateChange} linking={linking}>
<HeaderButtonsProvider stackType='native'>
<Navigator />
</HeaderButtonsProvider>
</NavigationContainer>
</IOSSafeAreaView>
</>
</TtsPlayer>
</SnackbarContainer>
</AppContextProvider>
</SafeAreaProvider>
Expand Down
10 changes: 10 additions & 0 deletions native/src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,18 @@ import MailIcon from '../../../assets/icons/mail.svg'
import MenuIcon from '../../../assets/icons/menu.svg'
import NewsIcon from '../../../assets/icons/news.svg'
import NoInternetIcon from '../../../assets/icons/no-internet.svg'
import NoSoundIcon from '../../../assets/icons/no-sound.svg'
import NoteIcon from '../../../assets/icons/note.svg'
import PauseIcon from '../../../assets/icons/pause.svg'
import PhoneIcon from '../../../assets/icons/phone.svg'
import PlayIcon from '../../../assets/icons/play.svg'
import PlaybackIcon from '../../../assets/icons/playback.svg'
import POIsIcon from '../../../assets/icons/pois.svg'
import RefreshIcon from '../../../assets/icons/refresh.svg'
import SadSmileyIcon from '../../../assets/icons/sad-smiley.svg'
import SearchIcon from '../../../assets/icons/search.svg'
import SecurityIcon from '../../../assets/icons/security.svg'
import SoundIcon from '../../../assets/icons/sound.svg'
import SprungbrettIcon from '../../../assets/icons/sprungbrett.svg'
import SupportIcon from '../../../assets/icons/support.svg'
import TuNewsActiveIcon from '../../../assets/icons/tu-news-active.svg'
Expand Down Expand Up @@ -63,6 +68,7 @@ export {
NewsIcon,
NoInternetIcon,
NoteIcon,
NoSoundIcon,
PhoneIcon,
POIsIcon,
PoiThumbnailPlaceholder,
Expand All @@ -71,10 +77,14 @@ export {
SadSmileyIcon,
SearchIcon,
SecurityIcon,
SoundIcon,
SprungbrettIcon,
SupportIcon,
TuNewsActiveIcon,
TuNewsInactiveIcon,
WarningIcon,
WebsiteIcon,
PauseIcon,
PlaybackIcon,
PlayIcon,
}
2 changes: 2 additions & 0 deletions native/src/components/Categories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { View } from 'react-native'
import { CATEGORIES_ROUTE, getCategoryTiles, RouteInformationType } from 'shared'
import { CategoriesMapModel, CategoryModel, CityModel } from 'shared/api'

import useTtsPlayer from '../hooks/useTtsPlayer'
import testID from '../testing/testID'
import { LanguageResourceCacheStateType } from '../utils/DataContainer'
import CategoryListItem from './CategoryListItem'
Expand Down Expand Up @@ -34,6 +35,7 @@ const Categories = ({
}: CategoriesProps): ReactElement => {
const children = categories.getChildren(category)
const cityCode = cityModel.code
useTtsPlayer(categories.isLeaf(category) ? category.content : '', categories.isLeaf(category) ? category.title : '')
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved

const navigateToCategory = ({ path }: { path: string }) =>
navigateTo({
Expand Down
10 changes: 10 additions & 0 deletions native/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import buildConfig from '../constants/buildConfig'
import dimensions from '../constants/dimensions'
import { AppContext } from '../contexts/AppContextProvider'
import useSnackbar from '../hooks/useSnackbar'
import useTtsPlayer from '../hooks/useTtsPlayer'
import createNavigateToFeedbackModal from '../navigation/createNavigateToFeedbackModal'
import navigateToLanguageChange from '../navigation/navigateToLanguageChange'
import sendTrackingSignal from '../utils/sendTrackingSignal'
Expand All @@ -50,6 +51,7 @@ enum HeaderButtonTitle {
Language = 'changeLanguage',
Location = 'changeLocation',
Search = 'search',
ReadAloud = 'readAloud',
Share = 'share',
Settings = 'settings',
Feedback = 'feedback',
Expand Down Expand Up @@ -82,6 +84,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 { setVisible: setTtsPlayerVisible, content } = useTtsPlayer()

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

const unsupportedLanguagesForTts = ['fa']
const ttsEnabled =
content && buildConfig().featureFlags.tts && !unsupportedLanguagesForTts.includes(languageCode)
? renderOverflowItem(t(`${HeaderButtonTitle.ReadAloud}`), () => setTtsPlayerVisible(true))
: []
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved

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)),
...[ttsEnabled],
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved
...(route.name !== NEWS_ROUTE ? [renderOverflowItem(HeaderButtonTitle.Feedback, navigateToFeedback)] : []),
...(route.name !== DISCLAIMER_ROUTE
? [renderOverflowItem(HeaderButtonTitle.Disclaimer, () => navigation.navigate(DISCLAIMER_ROUTE))]
Expand Down
2 changes: 2 additions & 0 deletions native/src/components/News.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { NavigationProps } from '../constants/NavigationTypes'
import { contentAlignment } from '../constants/contentDirection'
import useNavigate from '../hooks/useNavigate'
import useSetRouteTitle from '../hooks/useSetRouteTitle'
import useTtsPlayer from '../hooks/useTtsPlayer'
import Failure from './Failure'
import List from './List'
import LoadingSpinner from './LoadingSpinner'
Expand Down Expand Up @@ -62,6 +63,7 @@ const News = ({
}: NewsProps): ReactElement => {
const selectedNewsItem = news.find(_newsItem => _newsItem.id === newsId)
const { t } = useTranslation('news')
useTtsPlayer(selectedNewsItem?.content ?? '', selectedNewsItem?.title ?? 'News')
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved

const navigation = useNavigate().navigation as NavigationProps<NewsRouteType>
useSetRouteTitle({ navigation, title: getPageTitle(selectedNewsType, selectedNewsItem, t) })
Expand Down
4 changes: 3 additions & 1 deletion native/src/components/RemoteContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
} from '../constants/webview'
import { useAppContext } from '../hooks/useCityAppContext'
import useNavigate from '../hooks/useNavigate'
import useTtsPlayer from '../hooks/useTtsPlayer'
import renderHtml from '../utils/renderHtml'
import { log, reportError } from '../utils/sentry'
import Failure from './Failure'
Expand Down Expand Up @@ -65,123 +66,124 @@
const { settings, updateSettings } = useAppContext()
const { navigateTo } = useNavigate()
const { externalSourcePermissions } = settings
const { visible } = useTtsPlayer()
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved

// https://github.com/react-native-webview/react-native-webview/issues/1069#issuecomment-651699461
const defaultWebviewHeight = 1
const [webViewHeight, setWebViewHeight] = useState<number>(defaultWebviewHeight)
const theme = useTheme()
const { t } = useTranslation()
const { width: deviceWidth } = useWindowDimensions()

useEffect(() => {
// If it takes too long returning false in onShouldStartLoadWithRequest the webview loads the pressed url anyway on android.
// Therefore only set it to state and execute onLinkPress in useEffect.
if (pressedUrl) {
onLinkPress(pressedUrl)
setPressedUrl(null)
}
}, [onLinkPress, pressedUrl])

useEffect(() => {
if (webViewHeight !== defaultWebviewHeight || content.length === 0) {
onLoad()
}
}, [onLoad, webViewHeight, content])

// messages are triggered in renderHtml.ts
const onMessage = useCallback(
(event: WebViewMessageEvent) => {
const message = JSON.parse(event.nativeEvent.data)
if (message.type === HEIGHT_MESSAGE_TYPE && typeof message.height === 'number') {
setWebViewHeight(message.height)
return
}

if (message.type === OPEN_SETTINGS_MESSAGE_TYPE) {
navigateTo({ route: CONSENT_ROUTE })
return
}

if (message.type === ALLOW_EXTERNAL_SOURCE_MESSAGE_TYPE && typeof message.source === 'string') {
const source = message.source
const updatedSources = { ...externalSourcePermissions, [source]: true }
updateSettings({ externalSourcePermissions: updatedSources })
return
}

if (message.type === WARNING_MESSAGE_TYPE) {
log(message.message, 'warning')
} else {
const messageText: string | undefined = message.message
const error = new Error(messageText ? JSON.stringify(messageText) : 'Unknown message received from webview')
reportError(error)
setError(error.message)
}
},
[externalSourcePermissions, navigateTo, updateSettings],
)

const onShouldStartLoadWithRequest = useCallback(
(event: WebViewNavigation): boolean => {
if (buildConfig().supportedIframeSources.some(source => event.url.includes(source))) {
return true
}
if (event.url === new URL(resourceCacheUrl).href) {
// Needed on iOS for the initial load
return true
}
// block non click events on ios that come up with iframes to avoid opening the iframe source directly in browser
if (event.navigationType !== 'click' && Platform.OS === 'ios') {
return false
}
// If it takes too long returning false the webview loads the pressed url anyway on android.
// Therefore only set it to state and execute onLinkPress in useEffect.
setPressedUrl(event.url)
return false
},
[resourceCacheUrl],
)

if (content.length === 0) {
return null
}
if (error) {
return <Failure code={ErrorCode.UnknownError} />
}

return (
<WebView
source={{
baseUrl: resourceCacheUrl,
html: renderHtml(
content,
cacheDictionary,
buildConfig().supportedIframeSources,
theme,
language,
externalSourcePermissions,
t,
deviceWidth,
dimensions.pageContainerPaddingHorizontal,
),
}}
originWhitelist={['*']} // Needed by iOS to load the initial html
javaScriptEnabled
dataDetectorTypes='none'
userAgent={userAgent}
domStorageEnabled={false}
allowsFullscreenVideo
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
scrollEnabled={false} // To disable scrolling in iOS
onMessage={onMessage}
renderError={renderWebviewError}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
// To allow custom handling of link clicks in android
// https://github.com/react-native-webview/react-native-webview/issues/1869
setSupportMultipleWindows={false}
style={{
height: webViewHeight,
height: visible ? webViewHeight + dimensions.ttsPlayerHeight : webViewHeight,

Check warning on line 186 in native/src/components/RemoteContent.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ Getting worse: Complex Method

RemoteContent increases in cyclomatic complexity from 18 to 19, 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.
bahaaTuffaha marked this conversation as resolved.
Show resolved Hide resolved
opacity: loading ? LOADING_OPACITY : DEFAULT_OPACITY,
}}
/>
Expand Down
Loading