+1
__mocks__/expo-localization.js
+1
__mocks__/expo-localization.js
···
1
+
export const getLocales = jest.fn().mockResolvedValue([])
+4
-6
src/lib/api/feed-manip.ts
+4
-6
src/lib/api/feed-manip.ts
···
202
202
tuner: FeedTuner,
203
203
slices: FeedViewPostsSlice[],
204
204
): FeedViewPostsSlice[] => {
205
-
const origSlices = slices.concat()
205
+
if (!langsCode2.length) {
206
+
return slices
207
+
}
206
208
for (let i = slices.length - 1; i >= 0; i--) {
207
209
let hasPreferredLang = false
208
210
for (const item of slices[i].items) {
···
236
238
slices.splice(i, 1)
237
239
}
238
240
}
239
-
if (slices.length) {
240
-
return slices
241
-
}
242
-
// fallback: give everything if the language filter left nothing
243
-
return origSlices
241
+
return slices
244
242
}
245
243
}
246
244
}
+1
-1
src/locale/languages.ts
+1
-1
src/locale/languages.ts
···
23
23
{code3: 'alt', code2: '', name: 'Southern Altai'},
24
24
{code3: 'amh', code2: 'am', name: 'Amharic'},
25
25
{code3: 'ang', code2: '', name: 'English, Old (ca.450-1100)'},
26
-
{code3: 'anp ', code2: 'Angika', name: 'angika'},
26
+
{code3: 'anp ', code2: 'Angika', name: 'Angika'},
27
27
{code3: 'apa', code2: '', name: 'Apache languages'},
28
28
{code3: 'ara', code2: 'ar', name: 'Arabic'},
29
29
{
+12
src/state/models/feeds/posts.ts
+12
src/state/models/feeds/posts.ts
···
297
297
// used to linearize async modifications to state
298
298
lock = new AwaitLock()
299
299
300
+
// used to track if what's hot is coming up empty
301
+
emptyFetches = 0
302
+
300
303
// data
301
304
slices: PostsFeedSliceModel[] = []
302
305
···
603
606
) {
604
607
this.loadMoreCursor = res.data.cursor
605
608
this.hasMore = !!this.loadMoreCursor
609
+
if (replace) {
610
+
this.emptyFetches = 0
611
+
}
606
612
607
613
this.rootStore.me.follows.hydrateProfiles(
608
614
res.data.feed.map(item => item.post.author),
···
624
630
this.slices = toAppend
625
631
} else {
626
632
this.slices = this.slices.concat(toAppend)
633
+
}
634
+
if (toAppend.length === 0) {
635
+
this.emptyFetches++
636
+
if (this.emptyFetches >= 10) {
637
+
this.hasMore = false
638
+
}
627
639
}
628
640
})
629
641
}
+23
-16
src/state/models/ui/preferences.ts
+23
-16
src/state/models/ui/preferences.ts
···
2
2
import {getLocales} from 'expo-localization'
3
3
import {isObj, hasProp} from 'lib/type-guards'
4
4
import {ComAtprotoLabelDefs} from '@atproto/api'
5
+
import {LabelValGroup} from 'lib/labeling/types'
5
6
import {getLabelValueGroup} from 'lib/labeling/helpers'
6
-
import {
7
-
LabelValGroup,
8
-
UNKNOWN_LABEL_GROUP,
9
-
ILLEGAL_LABEL_GROUP,
10
-
} from 'lib/labeling/const'
7
+
import {UNKNOWN_LABEL_GROUP, ILLEGAL_LABEL_GROUP} from 'lib/labeling/const'
11
8
12
9
const deviceLocales = getLocales()
13
10
···
28
25
}
29
26
30
27
export class PreferencesModel {
31
-
_contentLanguages: string[] | undefined
28
+
contentLanguages: string[] =
29
+
deviceLocales?.map?.(locale => locale.languageCode) || []
32
30
contentLabels = new LabelPreferencesModel()
33
31
34
32
constructor() {
35
33
makeAutoObservable(this, {}, {autoBind: true})
36
34
}
37
35
38
-
// gives an array of BCP 47 language tags without region codes
39
-
get contentLanguages() {
40
-
if (this._contentLanguages) {
41
-
return this._contentLanguages
42
-
}
43
-
return deviceLocales.map(locale => locale.languageCode)
44
-
}
45
-
46
36
serialize() {
47
37
return {
48
-
contentLanguages: this._contentLanguages,
38
+
contentLanguages: this.contentLanguages,
49
39
contentLabels: this.contentLabels,
50
40
}
51
41
}
···
57
47
Array.isArray(v.contentLanguages) &&
58
48
typeof v.contentLanguages.every(item => typeof item === 'string')
59
49
) {
60
-
this._contentLanguages = v.contentLanguages
50
+
this.contentLanguages = v.contentLanguages
61
51
}
62
52
if (hasProp(v, 'contentLabels') && typeof v.contentLabels === 'object') {
63
53
Object.assign(this.contentLabels, v.contentLabels)
54
+
} else {
55
+
// default to the device languages
56
+
this.contentLanguages = deviceLocales.map(locale => locale.languageCode)
64
57
}
58
+
}
59
+
}
60
+
61
+
hasContentLanguage(code2: string) {
62
+
return this.contentLanguages.includes(code2)
63
+
}
64
+
65
+
toggleContentLanguage(code2: string) {
66
+
if (this.hasContentLanguage(code2)) {
67
+
this.contentLanguages = this.contentLanguages.filter(
68
+
lang => lang !== code2,
69
+
)
70
+
} else {
71
+
this.contentLanguages = this.contentLanguages.concat([code2])
65
72
}
66
73
}
67
74
+5
src/state/models/ui/shell.ts
+5
src/state/models/ui/shell.ts
···
85
85
name: 'content-filtering-settings'
86
86
}
87
87
88
+
export interface ContentLanguagesSettingsModal {
89
+
name: 'content-languages-settings'
90
+
}
91
+
88
92
export type Modal =
89
93
// Account
90
94
| AddAppPasswordModal
···
94
98
95
99
// Curation
96
100
| ContentFilteringSettingsModal
101
+
| ContentLanguagesSettingsModal
97
102
98
103
// Reporting
99
104
| ReportAccountModal
+1
-1
src/view/com/modals/ContentFilteringSettings.tsx
+1
-1
src/view/com/modals/ContentFilteringSettings.tsx
···
21
21
}, [store])
22
22
23
23
return (
24
-
<View testID="reportPostModal" style={[pal.view, styles.container]}>
24
+
<View testID="contentModerationModal" style={[pal.view, styles.container]}>
25
25
<Text style={[pal.text, styles.title]}>Content Moderation</Text>
26
26
<ScrollView style={styles.scrollContainer}>
27
27
<ContentLabelPref group="nsfw" />
+143
src/view/com/modals/ContentLanguagesSettings.tsx
+143
src/view/com/modals/ContentLanguagesSettings.tsx
···
1
+
import React from 'react'
2
+
import {StyleSheet, Pressable, View} from 'react-native'
3
+
import LinearGradient from 'react-native-linear-gradient'
4
+
import {observer} from 'mobx-react-lite'
5
+
import {ScrollView} from './util'
6
+
import {useStores} from 'state/index'
7
+
import {ToggleButton} from '../util/forms/ToggleButton'
8
+
import {s, colors, gradients} from 'lib/styles'
9
+
import {Text} from '../util/text/Text'
10
+
import {usePalette} from 'lib/hooks/usePalette'
11
+
import {isDesktopWeb} from 'platform/detection'
12
+
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../locale/languages'
13
+
14
+
export const snapPoints = ['100%']
15
+
16
+
export function Component({}: {}) {
17
+
const store = useStores()
18
+
const pal = usePalette('default')
19
+
const onPressDone = React.useCallback(() => {
20
+
store.shell.closeModal()
21
+
}, [store])
22
+
23
+
const languages = React.useMemo(() => {
24
+
const langs = LANGUAGES.filter(
25
+
lang =>
26
+
!!lang.code2.trim() &&
27
+
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
28
+
)
29
+
// sort so that selected languages are on top, then alphabetically
30
+
langs.sort((a, b) => {
31
+
const hasA = store.preferences.hasContentLanguage(a.code2)
32
+
const hasB = store.preferences.hasContentLanguage(b.code2)
33
+
if (hasA === hasB) return a.name.localeCompare(b.name)
34
+
if (hasA) return -1
35
+
return 1
36
+
})
37
+
return langs
38
+
}, [store])
39
+
40
+
return (
41
+
<View testID="contentLanguagesModal" style={[pal.view, styles.container]}>
42
+
<Text style={[pal.text, styles.title]}>Content Languages</Text>
43
+
<Text style={[pal.text, styles.description]}>
44
+
Which languages would you like to see in the What's Hot feed? (Leave
45
+
them all unchecked to see any language.)
46
+
</Text>
47
+
<ScrollView style={styles.scrollContainer}>
48
+
{languages.map(lang => (
49
+
<LanguageToggle
50
+
key={lang.code2}
51
+
code2={lang.code2}
52
+
name={lang.name}
53
+
/>
54
+
))}
55
+
<View style={styles.bottomSpacer} />
56
+
</ScrollView>
57
+
<View style={[styles.btnContainer, pal.borderDark]}>
58
+
<Pressable
59
+
testID="sendReportBtn"
60
+
onPress={onPressDone}
61
+
accessibilityRole="button"
62
+
accessibilityLabel="Confirm content language settings"
63
+
accessibilityHint="">
64
+
<LinearGradient
65
+
colors={[gradients.blueLight.start, gradients.blueLight.end]}
66
+
start={{x: 0, y: 0}}
67
+
end={{x: 1, y: 1}}
68
+
style={[styles.btn]}>
69
+
<Text style={[s.white, s.bold, s.f18]}>Done</Text>
70
+
</LinearGradient>
71
+
</Pressable>
72
+
</View>
73
+
</View>
74
+
)
75
+
}
76
+
77
+
const LanguageToggle = observer(
78
+
({code2, name}: {code2: string; name: string}) => {
79
+
const store = useStores()
80
+
const pal = usePalette('default')
81
+
82
+
const onPress = React.useCallback(() => {
83
+
store.preferences.toggleContentLanguage(code2)
84
+
}, [store, code2])
85
+
86
+
return (
87
+
<ToggleButton
88
+
label={name}
89
+
isSelected={store.preferences.contentLanguages.includes(code2)}
90
+
onPress={onPress}
91
+
style={[pal.border, styles.languageToggle]}
92
+
/>
93
+
)
94
+
},
95
+
)
96
+
97
+
const styles = StyleSheet.create({
98
+
container: {
99
+
flex: 1,
100
+
paddingTop: 20,
101
+
},
102
+
title: {
103
+
textAlign: 'center',
104
+
fontWeight: 'bold',
105
+
fontSize: 24,
106
+
marginBottom: 12,
107
+
},
108
+
description: {
109
+
textAlign: 'center',
110
+
paddingHorizontal: 16,
111
+
marginBottom: 10,
112
+
},
113
+
scrollContainer: {
114
+
flex: 1,
115
+
paddingHorizontal: 10,
116
+
},
117
+
bottomSpacer: {
118
+
height: isDesktopWeb ? 0 : 60,
119
+
},
120
+
btnContainer: {
121
+
paddingTop: 10,
122
+
paddingHorizontal: 10,
123
+
paddingBottom: isDesktopWeb ? 0 : 40,
124
+
borderTopWidth: isDesktopWeb ? 0 : 1,
125
+
},
126
+
127
+
languageToggle: {
128
+
borderTopWidth: 1,
129
+
borderRadius: 0,
130
+
paddingHorizontal: 0,
131
+
paddingVertical: 12,
132
+
},
133
+
134
+
btn: {
135
+
flexDirection: 'row',
136
+
alignItems: 'center',
137
+
justifyContent: 'center',
138
+
width: '100%',
139
+
borderRadius: 32,
140
+
padding: 14,
141
+
backgroundColor: colors.gray1,
142
+
},
143
+
})
+4
src/view/com/modals/Modal.tsx
+4
src/view/com/modals/Modal.tsx
···
21
21
import * as InviteCodesModal from './InviteCodes'
22
22
import * as AddAppPassword from './AddAppPasswords'
23
23
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
24
+
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
24
25
25
26
const DEFAULT_SNAPPOINTS = ['90%']
26
27
···
93
94
} else if (activeModal?.name === 'content-filtering-settings') {
94
95
snapPoints = ContentFilteringSettingsModal.snapPoints
95
96
element = <ContentFilteringSettingsModal.Component />
97
+
} else if (activeModal?.name === 'content-languages-settings') {
98
+
snapPoints = ContentLanguagesSettingsModal.snapPoints
99
+
element = <ContentLanguagesSettingsModal.Component />
96
100
} else {
97
101
return null
98
102
}
+3
src/view/com/modals/Modal.web.tsx
+3
src/view/com/modals/Modal.web.tsx
···
21
21
import * as InviteCodesModal from './InviteCodes'
22
22
import * as AddAppPassword from './AddAppPasswords'
23
23
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
24
+
import * as ContentLanguagesSettingsModal from './ContentLanguagesSettings'
24
25
25
26
export const ModalsContainer = observer(function ModalsContainer() {
26
27
const store = useStores()
···
84
85
element = <AddAppPassword.Component />
85
86
} else if (modal.name === 'content-filtering-settings') {
86
87
element = <ContentFilteringSettingsModal.Component />
88
+
} else if (modal.name === 'content-languages-settings') {
89
+
element = <ContentLanguagesSettingsModal.Component />
87
90
} else if (modal.name === 'alt-text-image') {
88
91
element = <AltTextImageModal.Component {...modal} />
89
92
} else if (modal.name === 'alt-text-image-read') {
-1
src/view/com/posts/FollowingEmptyState.tsx
-1
src/view/com/posts/FollowingEmptyState.tsx
+76
src/view/com/posts/WhatsHotEmptyState.tsx
+76
src/view/com/posts/WhatsHotEmptyState.tsx
···
1
+
import React from 'react'
2
+
import {StyleSheet, View} from 'react-native'
3
+
import {
4
+
FontAwesomeIcon,
5
+
FontAwesomeIconStyle,
6
+
} from '@fortawesome/react-native-fontawesome'
7
+
import {Text} from '../util/text/Text'
8
+
import {Button} from '../util/forms/Button'
9
+
import {MagnifyingGlassIcon} from 'lib/icons'
10
+
import {useStores} from 'state/index'
11
+
import {usePalette} from 'lib/hooks/usePalette'
12
+
import {s} from 'lib/styles'
13
+
14
+
export function WhatsHotEmptyState() {
15
+
const pal = usePalette('default')
16
+
const palInverted = usePalette('inverted')
17
+
const store = useStores()
18
+
19
+
const onPressSettings = React.useCallback(() => {
20
+
store.shell.openModal({name: 'content-languages-settings'})
21
+
}, [store])
22
+
23
+
return (
24
+
<View style={styles.emptyContainer}>
25
+
<View style={styles.emptyIconContainer}>
26
+
<MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} />
27
+
</View>
28
+
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
29
+
Your What's Hot feed is empty! This is because there aren't enough users
30
+
posting in your selected language.
31
+
</Text>
32
+
<Button type="inverted" style={styles.emptyBtn} onPress={onPressSettings}>
33
+
<Text type="lg-medium" style={palInverted.text}>
34
+
Update my settings
35
+
</Text>
36
+
<FontAwesomeIcon
37
+
icon="angle-right"
38
+
style={palInverted.text as FontAwesomeIconStyle}
39
+
size={14}
40
+
/>
41
+
</Button>
42
+
</View>
43
+
)
44
+
}
45
+
const styles = StyleSheet.create({
46
+
emptyContainer: {
47
+
height: '100%',
48
+
paddingVertical: 40,
49
+
paddingHorizontal: 30,
50
+
},
51
+
emptyIconContainer: {
52
+
marginBottom: 16,
53
+
},
54
+
emptyIcon: {
55
+
marginLeft: 'auto',
56
+
marginRight: 'auto',
57
+
},
58
+
emptyBtn: {
59
+
marginVertical: 20,
60
+
flexDirection: 'row',
61
+
alignItems: 'center',
62
+
justifyContent: 'space-between',
63
+
paddingVertical: 18,
64
+
paddingHorizontal: 24,
65
+
borderRadius: 30,
66
+
},
67
+
68
+
feedsTip: {
69
+
position: 'absolute',
70
+
left: 22,
71
+
},
72
+
feedsTipArrow: {
73
+
marginLeft: 32,
74
+
marginTop: 8,
75
+
},
76
+
})
+84
-66
src/view/screens/Home.tsx
+84
-66
src/view/screens/Home.tsx
···
9
9
import {useTabFocusEffect} from 'lib/hooks/useTabFocusEffect'
10
10
import {Feed} from '../com/posts/Feed'
11
11
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
12
+
import {WhatsHotEmptyState} from 'view/com/posts/WhatsHotEmptyState'
12
13
import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
13
14
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
14
15
import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
···
24
25
const POLL_FREQ = 30e3 // 30sec
25
26
26
27
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
27
-
export const HomeScreen = withAuthRequired((_opts: Props) => {
28
-
const store = useStores()
29
-
const [selectedPage, setSelectedPage] = React.useState(0)
28
+
export const HomeScreen = withAuthRequired(
29
+
observer((_opts: Props) => {
30
+
const store = useStores()
31
+
const [selectedPage, setSelectedPage] = React.useState(0)
32
+
const [initialLanguages] = React.useState(
33
+
store.preferences.contentLanguages,
34
+
)
30
35
31
-
const algoFeed = React.useMemo(() => {
32
-
const feed = new PostsFeedModel(store, 'goodstuff', {})
33
-
feed.setup()
34
-
return feed
35
-
}, [store])
36
+
const algoFeed: PostsFeedModel = React.useMemo(() => {
37
+
const feed = new PostsFeedModel(store, 'goodstuff', {})
38
+
feed.setup()
39
+
return feed
40
+
}, [store])
36
41
37
-
useFocusEffect(
38
-
React.useCallback(() => {
39
-
store.shell.setMinimalShellMode(false)
40
-
store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
41
-
return () => {
42
-
store.shell.setIsDrawerSwipeDisabled(false)
42
+
React.useEffect(() => {
43
+
// refresh whats hot when lang preferences change
44
+
if (initialLanguages !== store.preferences.contentLanguages) {
45
+
algoFeed.refresh()
43
46
}
44
-
}, [store, selectedPage]),
45
-
)
47
+
}, [initialLanguages, store.preferences.contentLanguages, algoFeed])
46
48
47
-
const onPageSelected = React.useCallback(
48
-
(index: number) => {
49
-
store.shell.setMinimalShellMode(false)
50
-
setSelectedPage(index)
51
-
store.shell.setIsDrawerSwipeDisabled(index > 0)
52
-
},
53
-
[store],
54
-
)
49
+
useFocusEffect(
50
+
React.useCallback(() => {
51
+
store.shell.setMinimalShellMode(false)
52
+
store.shell.setIsDrawerSwipeDisabled(selectedPage > 0)
53
+
return () => {
54
+
store.shell.setIsDrawerSwipeDisabled(false)
55
+
}
56
+
}, [store, selectedPage]),
57
+
)
55
58
56
-
const onPressSelected = React.useCallback(() => {
57
-
store.emitScreenSoftReset()
58
-
}, [store])
59
+
const onPageSelected = React.useCallback(
60
+
(index: number) => {
61
+
store.shell.setMinimalShellMode(false)
62
+
setSelectedPage(index)
63
+
store.shell.setIsDrawerSwipeDisabled(index > 0)
64
+
},
65
+
[store],
66
+
)
59
67
60
-
const renderTabBar = React.useCallback(
61
-
(props: RenderTabBarFnProps) => {
62
-
return (
63
-
<FeedsTabBar
64
-
{...props}
65
-
testID="homeScreenFeedTabs"
66
-
onPressSelected={onPressSelected}
67
-
/>
68
-
)
69
-
},
70
-
[onPressSelected],
71
-
)
68
+
const onPressSelected = React.useCallback(() => {
69
+
store.emitScreenSoftReset()
70
+
}, [store])
72
71
73
-
const renderFollowingEmptyState = React.useCallback(() => {
74
-
return <FollowingEmptyState />
75
-
}, [])
72
+
const renderTabBar = React.useCallback(
73
+
(props: RenderTabBarFnProps) => {
74
+
return (
75
+
<FeedsTabBar
76
+
{...props}
77
+
testID="homeScreenFeedTabs"
78
+
onPressSelected={onPressSelected}
79
+
/>
80
+
)
81
+
},
82
+
[onPressSelected],
83
+
)
76
84
77
-
const initialPage = store.me.followsCount === 0 ? 1 : 0
78
-
return (
79
-
<Pager
80
-
testID="homeScreen"
81
-
onPageSelected={onPageSelected}
82
-
renderTabBar={renderTabBar}
83
-
tabBarPosition="top"
84
-
initialPage={initialPage}>
85
-
<FeedPage
86
-
key="1"
87
-
testID="followingFeedPage"
88
-
isPageFocused={selectedPage === 0}
89
-
feed={store.me.mainFeed}
90
-
renderEmptyState={renderFollowingEmptyState}
91
-
/>
92
-
<FeedPage
93
-
key="2"
94
-
testID="whatshotFeedPage"
95
-
isPageFocused={selectedPage === 1}
96
-
feed={algoFeed}
97
-
/>
98
-
</Pager>
99
-
)
100
-
})
85
+
const renderFollowingEmptyState = React.useCallback(() => {
86
+
return <FollowingEmptyState />
87
+
}, [])
88
+
89
+
const renderWhatsHotEmptyState = React.useCallback(() => {
90
+
return <WhatsHotEmptyState />
91
+
}, [])
92
+
93
+
const initialPage = store.me.followsCount === 0 ? 1 : 0
94
+
return (
95
+
<Pager
96
+
testID="homeScreen"
97
+
onPageSelected={onPageSelected}
98
+
renderTabBar={renderTabBar}
99
+
tabBarPosition="top"
100
+
initialPage={initialPage}>
101
+
<FeedPage
102
+
key="1"
103
+
testID="followingFeedPage"
104
+
isPageFocused={selectedPage === 0}
105
+
feed={store.me.mainFeed}
106
+
renderEmptyState={renderFollowingEmptyState}
107
+
/>
108
+
<FeedPage
109
+
key="2"
110
+
testID="whatshotFeedPage"
111
+
isPageFocused={selectedPage === 1}
112
+
feed={algoFeed}
113
+
renderEmptyState={renderWhatsHotEmptyState}
114
+
/>
115
+
</Pager>
116
+
)
117
+
}),
118
+
)
101
119
102
120
const FeedPage = observer(
103
121
({
+23
-1
src/view/screens/Settings.tsx
+23
-1
src/view/screens/Settings.tsx
···
131
131
store.shell.openModal({name: 'content-filtering-settings'})
132
132
}, [track, store])
133
133
134
+
const onPressContentLanguages = React.useCallback(() => {
135
+
track('Settings:ContentlanguagesButtonClicked')
136
+
store.shell.openModal({name: 'content-languages-settings'})
137
+
}, [track, store])
138
+
134
139
const onPressSignout = React.useCallback(() => {
135
140
track('Settings:SignOutButtonClicked')
136
141
store.session.logout()
···
312
317
/>
313
318
</View>
314
319
<Text type="lg" style={pal.text}>
315
-
App Passwords
320
+
App passwords
316
321
</Text>
317
322
</Link>
323
+
<TouchableOpacity
324
+
testID="contentLanguagesBtn"
325
+
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}
326
+
onPress={isSwitching ? undefined : onPressContentLanguages}
327
+
accessibilityRole="button"
328
+
accessibilityHint="Content languages"
329
+
accessibilityLabel="Opens configurable content language settings">
330
+
<View style={[styles.iconContainer, pal.btn]}>
331
+
<FontAwesomeIcon
332
+
icon="language"
333
+
style={pal.text as FontAwesomeIconStyle}
334
+
/>
335
+
</View>
336
+
<Text type="lg" style={pal.text}>
337
+
Content languages
338
+
</Text>
339
+
</TouchableOpacity>
318
340
<TouchableOpacity
319
341
testID="changeHandleBtn"
320
342
style={[styles.linkCard, pal.view, isSwitching && styles.dimmed]}