Language select final tweaks (#8914)
* [APP-1303] Redesign/refactor post language select (#8884) * Nightly source-language update * Nightly source-language update * [APP-1303] Redesign/refactor post language select * update: stylesheets.create to use the latest structure * update styles to modern structure * update: dialog breakpoints on web and delete depricated language modals * remove unused post languages settings dialog * restructure Post languages dialog * place the Dialog.Close inside the Dialog.ScrollableInner * add: language search * update search and language variables for clarity * fix: memoize language state lists * chore: add comments * update proper colors to the background * add back older error boundary * add: tweaks to the mobile and web responsiveness * add tweaks to center the container * update labels * update button and border * added translation updates * Update: text input to reuse search input * remove unused file * update: web breakpoints * run eslint and prettier --------- Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com> Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local> Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain> * rm old file * sort out styles, add FlatListFooter component * rm cancel button in favor of search input X * get dialog height working on iOS * delete `DropdownButton` * hide scroll indicators on android * ios scroll indicator insets * get footer sorta working on android * change button color on press * rm empty file --------- Co-authored-by: Anastasiya Uraleva <anastasiyauraleva@gmail.com> Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com> Co-authored-by: Anastasiya Uraleva <anastasiya@Anastasiyas-MacBook-Pro.local> Co-authored-by: Anastasiya Uraleva <anastasiya@Mac.localdomain>
This commit is contained in:
@@ -1016,6 +1016,9 @@ export const atoms = {
|
|||||||
block: web({
|
block: web({
|
||||||
display: 'block',
|
display: 'block',
|
||||||
}),
|
}),
|
||||||
|
contents: web({
|
||||||
|
display: 'contents',
|
||||||
|
}),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Transition
|
* Transition
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
KeyboardAwareScrollView,
|
KeyboardAwareScrollView,
|
||||||
useKeyboardHandler,
|
useKeyboardHandler,
|
||||||
|
useReanimatedKeyboardAnimation,
|
||||||
} from 'react-native-keyboard-controller'
|
} from 'react-native-keyboard-controller'
|
||||||
import {runOnJS} from 'react-native-reanimated'
|
import Animated, {
|
||||||
import {type ReanimatedScrollEvent} from 'react-native-reanimated/lib/typescript/hook/commonTypes'
|
runOnJS,
|
||||||
|
type ScrollEvent,
|
||||||
|
useAnimatedStyle,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -26,7 +30,7 @@ import {isAndroid, isIOS} from '#/platform/detection'
|
|||||||
import {useA11y} from '#/state/a11y'
|
import {useA11y} from '#/state/a11y'
|
||||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||||
import {List, type ListMethods, type ListProps} from '#/view/com/util/List'
|
import {List, type ListMethods, type ListProps} from '#/view/com/util/List'
|
||||||
import {atoms as a, tokens, useTheme} from '#/alf'
|
import {atoms as a, ios, platform, tokens, useTheme} from '#/alf'
|
||||||
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
import {useThemeName} from '#/alf/util/useColorModeTheme'
|
||||||
import {Context, useDialogContext} from '#/components/Dialog/context'
|
import {Context, useDialogContext} from '#/components/Dialog/context'
|
||||||
import {
|
import {
|
||||||
@@ -256,6 +260,7 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
]}
|
]}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
showsVerticalScrollIndicator={isAndroid ? false : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||||
bottomOffset={30}
|
bottomOffset={30}
|
||||||
@@ -275,12 +280,15 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
ListProps<any> & {
|
ListProps<any> & {
|
||||||
webInnerStyle?: StyleProp<ViewStyle>
|
webInnerStyle?: StyleProp<ViewStyle>
|
||||||
webInnerContentContainerStyle?: StyleProp<ViewStyle>
|
webInnerContentContainerStyle?: StyleProp<ViewStyle>
|
||||||
|
footer?: React.ReactNode
|
||||||
}
|
}
|
||||||
>(function InnerFlatList({style, ...props}, ref) {
|
>(function InnerFlatList({footer, style, ...props}, ref) {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||||
|
|
||||||
const onScroll = (e: ReanimatedScrollEvent) => {
|
useEnableKeyboardController(isIOS)
|
||||||
|
|
||||||
|
const onScroll = (e: ScrollEvent) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
if (!isAndroid) {
|
if (!isAndroid) {
|
||||||
return
|
return
|
||||||
@@ -300,13 +308,54 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
|
||||||
ListFooterComponent={<View style={{height: insets.bottom + 100}} />}
|
ListFooterComponent={<View style={{height: insets.bottom + 100}} />}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
showsVerticalScrollIndicator={isAndroid ? false : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
style={[style]}
|
style={[a.h_full, style]}
|
||||||
/>
|
/>
|
||||||
|
{footer}
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function FlatListFooter({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {top, bottom} = useSafeAreaInsets()
|
||||||
|
const {height} = useReanimatedKeyboardAnimation()
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
if (!isIOS) return {}
|
||||||
|
return {
|
||||||
|
transform: [{translateY: Math.min(0, height.get() + bottom - 10)}],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.bottom_0,
|
||||||
|
a.w_full,
|
||||||
|
a.z_10,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.bg,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.pt_md,
|
||||||
|
{
|
||||||
|
paddingBottom: platform({
|
||||||
|
ios: tokens.space.md + bottom,
|
||||||
|
android: tokens.space.md + bottom + top,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
// TODO: had to admit defeat here, but we should
|
||||||
|
// try and get this to work for Android as well -sfn
|
||||||
|
ios(animatedStyle),
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</Animated.View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Handle({difference = false}: {difference?: boolean}) {
|
export function Handle({difference = false}: {difference?: boolean}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export * from '#/components/Dialog/types'
|
|||||||
export * from '#/components/Dialog/utils'
|
export * from '#/components/Dialog/utils'
|
||||||
export {Input} from '#/components/forms/TextField'
|
export {Input} from '#/components/forms/TextField'
|
||||||
|
|
||||||
|
// 100 minus 10vh of paddingVertical
|
||||||
|
export const WEB_DIALOG_HEIGHT = '80vh'
|
||||||
|
|
||||||
const stopPropagation = (e: any) => e.stopPropagation()
|
const stopPropagation = (e: any) => e.stopPropagation()
|
||||||
const preventDefault = (e: any) => e.preventDefault()
|
const preventDefault = (e: any) => e.preventDefault()
|
||||||
|
|
||||||
@@ -215,9 +218,17 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
FlatListProps<any> & {label: string} & {
|
FlatListProps<any> & {label: string} & {
|
||||||
webInnerStyle?: StyleProp<ViewStyle>
|
webInnerStyle?: StyleProp<ViewStyle>
|
||||||
webInnerContentContainerStyle?: StyleProp<ViewStyle>
|
webInnerContentContainerStyle?: StyleProp<ViewStyle>
|
||||||
|
footer?: React.ReactNode
|
||||||
}
|
}
|
||||||
>(function InnerFlatList(
|
>(function InnerFlatList(
|
||||||
{label, style, webInnerStyle, webInnerContentContainerStyle, ...props},
|
{
|
||||||
|
label,
|
||||||
|
style,
|
||||||
|
webInnerStyle,
|
||||||
|
webInnerContentContainerStyle,
|
||||||
|
footer,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
@@ -227,8 +238,7 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
style={[
|
style={[
|
||||||
a.overflow_hidden,
|
a.overflow_hidden,
|
||||||
a.px_0,
|
a.px_0,
|
||||||
// 100 minus 10vh of paddingVertical
|
web({maxHeight: WEB_DIALOG_HEIGHT}),
|
||||||
web({maxHeight: '80vh'}),
|
|
||||||
webInnerStyle,
|
webInnerStyle,
|
||||||
]}
|
]}
|
||||||
contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
|
contentContainerStyle={[a.h_full, a.px_0, webInnerContentContainerStyle]}>
|
||||||
@@ -237,10 +247,32 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
|
style={[a.h_full, gtMobile ? a.px_2xl : a.px_xl, flatten(style)]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
{footer}
|
||||||
</Inner>
|
</Inner>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export function FlatListFooter({children}: {children: React.ReactNode}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
a.bottom_0,
|
||||||
|
a.w_full,
|
||||||
|
a.z_10,
|
||||||
|
t.atoms.bg,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_md,
|
||||||
|
]}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function Close() {
|
export function Close() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const {close} = React.useContext(Context)
|
const {close} = React.useContext(Context)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View, type ViewStyle} from 'react-native'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import Animated, {LinearTransition} from 'react-native-reanimated'
|
import Animated, {LinearTransition} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
import {HITSLOP_10} from '#/lib/constants'
|
||||||
@@ -59,6 +59,7 @@ export type GroupProps = React.PropsWithChildren<{
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
onChange: (value: string[]) => void
|
onChange: (value: string[]) => void
|
||||||
label: string
|
label: string
|
||||||
|
style?: StyleProp<ViewStyle>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type ItemProps = ViewStyleProp & {
|
export type ItemProps = ViewStyleProp & {
|
||||||
@@ -84,6 +85,7 @@ export function Group({
|
|||||||
type = 'checkbox',
|
type = 'checkbox',
|
||||||
maxSelections,
|
maxSelections,
|
||||||
label,
|
label,
|
||||||
|
style,
|
||||||
}: GroupProps) {
|
}: GroupProps) {
|
||||||
const groupRole = type === 'radio' ? 'radiogroup' : undefined
|
const groupRole = type === 'radio' ? 'radiogroup' : undefined
|
||||||
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
|
const values = type === 'radio' ? providedValues.slice(0, 1) : providedValues
|
||||||
@@ -136,7 +138,7 @@ export function Group({
|
|||||||
return (
|
return (
|
||||||
<GroupContext.Provider value={context}>
|
<GroupContext.Provider value={context}>
|
||||||
<View
|
<View
|
||||||
style={[a.w_full]}
|
style={[a.w_full, style]}
|
||||||
role={groupRole}
|
role={groupRole}
|
||||||
{...(groupRole === 'radiogroup'
|
{...(groupRole === 'radiogroup'
|
||||||
? {
|
? {
|
||||||
|
|||||||
+3
-1
@@ -7,17 +7,19 @@ export function MagnifyingGlassIcon({
|
|||||||
style,
|
style,
|
||||||
size,
|
size,
|
||||||
strokeWidth = 2,
|
strokeWidth = 2,
|
||||||
|
color = 'currentColor',
|
||||||
}: {
|
}: {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
size?: string | number
|
size?: string | number
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
|
color?: string
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Svg
|
<Svg
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
strokeWidth={strokeWidth}
|
strokeWidth={strokeWidth}
|
||||||
stroke="currentColor"
|
stroke={color}
|
||||||
width={size || 24}
|
width={size || 24}
|
||||||
height={size || 24}
|
height={size || 24}
|
||||||
style={style}>
|
style={style}>
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ export interface ContentLanguagesSettingsModal {
|
|||||||
name: 'content-languages-settings'
|
name: 'content-languages-settings'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PostLanguagesSettingsModal {
|
|
||||||
name: 'post-languages-settings'
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated DO NOT ADD NEW MODALS
|
* @deprecated DO NOT ADD NEW MODALS
|
||||||
*/
|
*/
|
||||||
@@ -48,7 +44,6 @@ export type Modal =
|
|||||||
|
|
||||||
// Curation
|
// Curation
|
||||||
| ContentLanguagesSettingsModal
|
| ContentLanguagesSettingsModal
|
||||||
| PostLanguagesSettingsModal
|
|
||||||
|
|
||||||
// Lists
|
// Lists
|
||||||
| CreateOrEditListModal
|
| CreateOrEditListModal
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ import {LabelsBtn} from '#/view/com/composer/labels/LabelsBtn'
|
|||||||
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
import {Gallery} from '#/view/com/composer/photos/Gallery'
|
||||||
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
|
import {OpenCameraBtn} from '#/view/com/composer/photos/OpenCameraBtn'
|
||||||
import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn'
|
import {SelectGifBtn} from '#/view/com/composer/photos/SelectGifBtn'
|
||||||
import {SelectLangBtn} from '#/view/com/composer/select-language/SelectLangBtn'
|
import {SelectPostLanguagesBtn} from '#/view/com/composer/select-language/SelectPostLanguagesDialog'
|
||||||
import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
|
import {SuggestedLanguage} from '#/view/com/composer/select-language/SuggestedLanguage'
|
||||||
// TODO: Prevent naming components that coincide with RN primitives
|
// TODO: Prevent naming components that coincide with RN primitives
|
||||||
// due to linting false positives
|
// due to linting false positives
|
||||||
@@ -1453,7 +1453,7 @@ function ComposerFooter({
|
|||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<SelectLangBtn />
|
<SelectPostLanguagesBtn />
|
||||||
<CharProgress
|
<CharProgress
|
||||||
count={post.shortenedGraphemeLength}
|
count={post.shortenedGraphemeLength}
|
||||||
style={{width: 65}}
|
style={{width: 65}}
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import {useCallback, useMemo} from 'react'
|
|
||||||
import {Keyboard, StyleSheet} from 'react-native'
|
|
||||||
import {
|
|
||||||
FontAwesomeIcon,
|
|
||||||
FontAwesomeIconStyle,
|
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
|
|
||||||
import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {isNative} from '#/platform/detection'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {
|
|
||||||
hasPostLanguage,
|
|
||||||
toPostLanguages,
|
|
||||||
useLanguagePrefs,
|
|
||||||
useLanguagePrefsApi,
|
|
||||||
} from '#/state/preferences/languages'
|
|
||||||
import {
|
|
||||||
DropdownButton,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownItemButton,
|
|
||||||
} from '#/view/com/util/forms/DropdownButton'
|
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
|
||||||
import {codeToLanguageName} from '../../../../locale/helpers'
|
|
||||||
|
|
||||||
export function SelectLangBtn() {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {openModal} = useModalControls()
|
|
||||||
const langPrefs = useLanguagePrefs()
|
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
|
||||||
|
|
||||||
const onPressMore = useCallback(async () => {
|
|
||||||
if (isNative) {
|
|
||||||
if (Keyboard.isVisible()) {
|
|
||||||
Keyboard.dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
openModal({name: 'post-languages-settings'})
|
|
||||||
}, [openModal])
|
|
||||||
|
|
||||||
const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
|
|
||||||
const items: DropdownItem[] = useMemo(() => {
|
|
||||||
let arr: DropdownItemButton[] = []
|
|
||||||
|
|
||||||
function add(commaSeparatedLangCodes: string) {
|
|
||||||
const langCodes = commaSeparatedLangCodes.split(',')
|
|
||||||
const langName = langCodes
|
|
||||||
.map(code => codeToLanguageName(code, langPrefs.appLanguage))
|
|
||||||
.join(' + ')
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Filter out any duplicates
|
|
||||||
*/
|
|
||||||
if (arr.find((item: DropdownItemButton) => item.label === langName)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
arr.push({
|
|
||||||
icon:
|
|
||||||
langCodes.every(code =>
|
|
||||||
hasPostLanguage(langPrefs.postLanguage, code),
|
|
||||||
) && langCodes.length === postLanguagesPref.length
|
|
||||||
? ['fas', 'circle-dot']
|
|
||||||
: ['far', 'circle'],
|
|
||||||
label: langName,
|
|
||||||
onPress() {
|
|
||||||
setLangPrefs.setPostLanguage(commaSeparatedLangCodes)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postLanguagesPref.length) {
|
|
||||||
/*
|
|
||||||
* Re-join here after sanitization bc postLanguageHistory is an array of
|
|
||||||
* comma-separated strings too
|
|
||||||
*/
|
|
||||||
add(langPrefs.postLanguage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// comma-separted strings of lang codes that have been used in the past
|
|
||||||
for (const lang of langPrefs.postLanguageHistory) {
|
|
||||||
add(lang)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
{heading: true, label: _(msg`Post language`)},
|
|
||||||
...arr.slice(0, 6),
|
|
||||||
{sep: true},
|
|
||||||
{
|
|
||||||
label: _(msg`Other...`),
|
|
||||||
onPress: onPressMore,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}, [onPressMore, langPrefs, setLangPrefs, postLanguagesPref, _])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
type="bare"
|
|
||||||
testID="selectLangBtn"
|
|
||||||
items={items}
|
|
||||||
openUpwards
|
|
||||||
style={styles.button}
|
|
||||||
hitSlop={LANG_DROPDOWN_HITSLOP}
|
|
||||||
accessibilityLabel={_(msg`Language selection`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
{postLanguagesPref.length > 0 ? (
|
|
||||||
<Text type="lg-bold" style={[pal.link, styles.label]} numberOfLines={1}>
|
|
||||||
{postLanguagesPref
|
|
||||||
.map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
|
|
||||||
.join(', ')}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
icon="language"
|
|
||||||
style={pal.link as FontAwesomeIconStyle}
|
|
||||||
size={26}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DropdownButton>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
button: {
|
|
||||||
marginHorizontal: 15,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
maxWidth: 100,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
|
import {Keyboard, useWindowDimensions, View} from 'react-native'
|
||||||
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {LANG_DROPDOWN_HITSLOP} from '#/lib/constants'
|
||||||
|
import {languageName} from '#/locale/helpers'
|
||||||
|
import {codeToLanguageName} from '#/locale/helpers'
|
||||||
|
import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages'
|
||||||
|
import {isNative, isWeb} from '#/platform/detection'
|
||||||
|
import {
|
||||||
|
toPostLanguages,
|
||||||
|
useLanguagePrefs,
|
||||||
|
useLanguagePrefsApi,
|
||||||
|
} from '#/state/preferences/languages'
|
||||||
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
|
import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
|
||||||
|
import {atoms as a, useTheme, web} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
|
import * as Dialog from '#/components/Dialog'
|
||||||
|
import {SearchInput} from '#/components/forms/SearchInput'
|
||||||
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
|
import {Globe_Stroke2_Corner0_Rounded as GlobeIcon} from '#/components/icons/Globe'
|
||||||
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
|
export function SelectPostLanguagesBtn() {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const langPrefs = useLanguagePrefs()
|
||||||
|
const t = useTheme()
|
||||||
|
const control = Dialog.useDialogControl()
|
||||||
|
|
||||||
|
const onPressMore = useCallback(async () => {
|
||||||
|
if (isNative) {
|
||||||
|
if (Keyboard.isVisible()) {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
control.open()
|
||||||
|
}, [control])
|
||||||
|
|
||||||
|
const postLanguagesPref = toPostLanguages(langPrefs.postLanguage)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
testID="selectLangBtn"
|
||||||
|
onPress={onPressMore}
|
||||||
|
size="small"
|
||||||
|
hitSlop={LANG_DROPDOWN_HITSLOP}
|
||||||
|
label={_(
|
||||||
|
msg({
|
||||||
|
message: `Post language selection`,
|
||||||
|
comment: `Accessibility label for button that opens dialog to choose post language settings`,
|
||||||
|
}),
|
||||||
|
)}
|
||||||
|
accessibilityHint={_(msg`Opens post language settings`)}
|
||||||
|
style={[a.mx_md]}>
|
||||||
|
{({pressed, hovered, focused}) => {
|
||||||
|
const color =
|
||||||
|
pressed || hovered || focused
|
||||||
|
? t.palette.primary_300
|
||||||
|
: t.palette.primary_500
|
||||||
|
if (postLanguagesPref.length > 0) {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
{color},
|
||||||
|
a.font_bold,
|
||||||
|
a.text_sm,
|
||||||
|
a.leading_snug,
|
||||||
|
{maxWidth: 100},
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
{postLanguagesPref
|
||||||
|
.map(lang => codeToLanguageName(lang, langPrefs.appLanguage))
|
||||||
|
.join(', ')}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <GlobeIcon size="xs" style={{color}} />
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<LanguageDialog control={control} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LanguageDialog({control}: {control: Dialog.DialogControlProps}) {
|
||||||
|
const {height} = useWindowDimensions()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
|
||||||
|
const renderErrorBoundary = useCallback(
|
||||||
|
(error: any) => <DialogError details={String(error)} />,
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
nativeOptions={{minHeight: height - insets.top}}>
|
||||||
|
<Dialog.Handle />
|
||||||
|
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||||
|
<PostLanguagesSettingsDialogInner />
|
||||||
|
</ErrorBoundary>
|
||||||
|
</Dialog.Outer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostLanguagesSettingsDialogInner() {
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
const [headerHeight, setHeaderHeight] = useState(0)
|
||||||
|
|
||||||
|
const allowedLanguages = useMemo(() => {
|
||||||
|
const uniqueLanguagesMap = LANGUAGES.filter(lang => !!lang.code2).reduce(
|
||||||
|
(acc, lang) => {
|
||||||
|
acc[lang.code2] = lang
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, Language>,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Object.values(uniqueLanguagesMap)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const langPrefs = useLanguagePrefs()
|
||||||
|
const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>(
|
||||||
|
langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage],
|
||||||
|
)
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
|
||||||
|
const setLangPrefs = useLanguagePrefsApi()
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
control.close(() => {
|
||||||
|
let langsString = checkedLanguagesCode2.join(',')
|
||||||
|
if (!langsString) {
|
||||||
|
langsString = langPrefs.primaryLanguage
|
||||||
|
}
|
||||||
|
setLangPrefs.setPostLanguage(langsString)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(@elijaharita): Displayed languages are split into 3 lists for
|
||||||
|
// ordering.
|
||||||
|
const displayedLanguages = useMemo(() => {
|
||||||
|
function mapCode2List(code2List: string[]) {
|
||||||
|
return code2List.map(code2 => LANGUAGES_MAP_CODE2[code2]).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE(@elijaharita): Get recent language codes and map them to language
|
||||||
|
// objects. Both the user account's saved language history and the current
|
||||||
|
// checked languages are displayed here.
|
||||||
|
const recentLanguagesCode2 =
|
||||||
|
Array.from(
|
||||||
|
new Set([...checkedLanguagesCode2, ...langPrefs.postLanguageHistory]),
|
||||||
|
).slice(0, 5) || []
|
||||||
|
const recentLanguages = mapCode2List(recentLanguagesCode2)
|
||||||
|
|
||||||
|
// NOTE(@elijaharita): helper functions
|
||||||
|
const matchesSearch = (lang: Language) =>
|
||||||
|
lang.name.toLowerCase().includes(search.toLowerCase())
|
||||||
|
const isChecked = (lang: Language) =>
|
||||||
|
checkedLanguagesCode2.includes(lang.code2)
|
||||||
|
const isInRecents = (lang: Language) =>
|
||||||
|
recentLanguagesCode2.includes(lang.code2)
|
||||||
|
|
||||||
|
const checkedRecent = recentLanguages.filter(isChecked)
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
// NOTE(@elijaharita): if a search is active, we ALWAYS show checked
|
||||||
|
// items, as well as any items that match the search.
|
||||||
|
const uncheckedRecent = recentLanguages
|
||||||
|
.filter(lang => !isChecked(lang))
|
||||||
|
.filter(matchesSearch)
|
||||||
|
const unchecked = allowedLanguages.filter(lang => !isChecked(lang))
|
||||||
|
const all = unchecked
|
||||||
|
.filter(matchesSearch)
|
||||||
|
.filter(lang => !isInRecents(lang))
|
||||||
|
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
checkedRecent,
|
||||||
|
uncheckedRecent,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// NOTE(@elijaharita): if no search is active, we show everything.
|
||||||
|
const uncheckedRecent = recentLanguages.filter(lang => !isChecked(lang))
|
||||||
|
const all = allowedLanguages
|
||||||
|
.filter(lang => !recentLanguagesCode2.includes(lang.code2))
|
||||||
|
.filter(lang => !isInRecents(lang))
|
||||||
|
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
checkedRecent,
|
||||||
|
uncheckedRecent,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
allowedLanguages,
|
||||||
|
search,
|
||||||
|
langPrefs.postLanguageHistory,
|
||||||
|
checkedLanguagesCode2,
|
||||||
|
])
|
||||||
|
|
||||||
|
const listHeader = (
|
||||||
|
<View
|
||||||
|
style={[a.pb_xs, t.atoms.bg, isNative && a.pt_2xl]}
|
||||||
|
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}>
|
||||||
|
<View style={[a.flex_row, a.w_full, a.justify_between]}>
|
||||||
|
<View>
|
||||||
|
<Text
|
||||||
|
nativeID="dialog-title"
|
||||||
|
style={[
|
||||||
|
t.atoms.text,
|
||||||
|
a.text_left,
|
||||||
|
a.font_bold,
|
||||||
|
a.text_xl,
|
||||||
|
a.mb_sm,
|
||||||
|
]}>
|
||||||
|
<Trans>Choose Post Languages</Trans>
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
nativeID="dialog-description"
|
||||||
|
style={[
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.text_left,
|
||||||
|
a.text_md,
|
||||||
|
a.mb_lg,
|
||||||
|
]}>
|
||||||
|
<Trans>Select up to 3 languages used in this post</Trans>
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{isWeb && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Close dialog`)}
|
||||||
|
onPress={handleClose}>
|
||||||
|
<ButtonIcon icon={XIcon} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.w_full, a.flex_row, a.align_stretch, a.gap_xs, a.pb_0]}>
|
||||||
|
<SearchInput
|
||||||
|
value={search}
|
||||||
|
onChangeText={setSearch}
|
||||||
|
placeholder={_(msg`Search languages`)}
|
||||||
|
label={_(msg`Search languages`)}
|
||||||
|
maxLength={50}
|
||||||
|
onClearText={() => setSearch('')}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
|
||||||
|
const isCheckedRecentEmpty =
|
||||||
|
displayedLanguages.checkedRecent.length > 0 ||
|
||||||
|
displayedLanguages.uncheckedRecent.length > 0
|
||||||
|
|
||||||
|
const isDisplayedLanguagesEmpty = displayedLanguages.all.length === 0
|
||||||
|
|
||||||
|
const flatListData = [
|
||||||
|
...(isCheckedRecentEmpty
|
||||||
|
? [{type: 'header', label: _(msg`Recently used`)}]
|
||||||
|
: []),
|
||||||
|
...displayedLanguages.checkedRecent.map(lang => ({type: 'item', lang})),
|
||||||
|
...displayedLanguages.uncheckedRecent.map(lang => ({type: 'item', lang})),
|
||||||
|
...(isDisplayedLanguagesEmpty
|
||||||
|
? []
|
||||||
|
: [{type: 'header', label: _(msg`All languages`)}]),
|
||||||
|
...displayedLanguages.all.map(lang => ({type: 'item', lang})),
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle.Group
|
||||||
|
values={checkedLanguagesCode2}
|
||||||
|
onChange={setCheckedLanguagesCode2}
|
||||||
|
type="checkbox"
|
||||||
|
maxSelections={3}
|
||||||
|
label={_(msg`Select languages`)}
|
||||||
|
style={web([a.contents])}>
|
||||||
|
<Dialog.InnerFlatList
|
||||||
|
data={flatListData}
|
||||||
|
ListHeaderComponent={listHeader}
|
||||||
|
stickyHeaderIndices={[0]}
|
||||||
|
contentContainerStyle={[a.gap_0]}
|
||||||
|
style={[isNative && a.px_lg, web({paddingBottom: 120})]}
|
||||||
|
scrollIndicatorInsets={{top: headerHeight}}
|
||||||
|
renderItem={({item, index}) => {
|
||||||
|
if (item.type === 'header') {
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
key={index}
|
||||||
|
style={[
|
||||||
|
a.px_0,
|
||||||
|
a.py_md,
|
||||||
|
a.font_bold,
|
||||||
|
a.text_xs,
|
||||||
|
t.atoms.text_contrast_low,
|
||||||
|
a.pt_3xl,
|
||||||
|
]}>
|
||||||
|
{item.label}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const lang = item.lang
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Toggle.Item
|
||||||
|
key={lang.code2}
|
||||||
|
name={lang.code2}
|
||||||
|
label={languageName(lang, langPrefs.appLanguage)}
|
||||||
|
style={[
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.border_b,
|
||||||
|
a.rounded_0,
|
||||||
|
a.px_0,
|
||||||
|
a.py_md,
|
||||||
|
]}>
|
||||||
|
<Toggle.LabelText style={[a.flex_1]}>
|
||||||
|
{languageName(lang, langPrefs.appLanguage)}
|
||||||
|
</Toggle.LabelText>
|
||||||
|
<Toggle.Checkbox />
|
||||||
|
</Toggle.Item>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<Dialog.FlatListFooter>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close dialog`)}
|
||||||
|
onPress={handleClose}
|
||||||
|
color="primary"
|
||||||
|
size="large">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Done</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</Dialog.FlatListFooter>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Toggle.Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogError({details}: {details?: string}) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
const control = Dialog.useDialogContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.ScrollableInner
|
||||||
|
style={a.gap_md}
|
||||||
|
label={_(msg`An error has occurred`)}>
|
||||||
|
<Dialog.Close />
|
||||||
|
<ErrorScreen
|
||||||
|
title={_(msg`Oh no!`)}
|
||||||
|
message={_(
|
||||||
|
msg`There was an unexpected issue in the application. Please let us know if this happened to you!`,
|
||||||
|
)}
|
||||||
|
details={details}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Close dialog`)}
|
||||||
|
onPress={() => control.close()}
|
||||||
|
color="primary"
|
||||||
|
size="large">
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Close</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
</Dialog.ScrollableInner>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,7 +11,6 @@ import * as CreateOrEditListModal from './CreateOrEditList'
|
|||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
|
||||||
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
import * as UserAddRemoveListsModal from './UserAddRemoveLists'
|
||||||
|
|
||||||
const DEFAULT_SNAPPOINTS = ['90%']
|
const DEFAULT_SNAPPOINTS = ['90%']
|
||||||
@@ -60,9 +59,6 @@ export function ModalsContainer() {
|
|||||||
} else if (activeModal?.name === 'content-languages-settings') {
|
} else if (activeModal?.name === 'content-languages-settings') {
|
||||||
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
snapPoints = ContentLanguagesSettingsModal.snapPoints
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
} else if (activeModal?.name === 'post-languages-settings') {
|
|
||||||
snapPoints = PostLanguagesSettingsModal.snapPoints
|
|
||||||
element = <PostLanguagesSettingsModal.Component />
|
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import * as CreateOrEditListModal from './CreateOrEditList'
|
|||||||
import * as DeleteAccountModal from './DeleteAccount'
|
import * as DeleteAccountModal from './DeleteAccount'
|
||||||
import * as InviteCodesModal from './InviteCodes'
|
import * as InviteCodesModal from './InviteCodes'
|
||||||
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
import * as ContentLanguagesSettingsModal from './lang-settings/ContentLanguagesSettings'
|
||||||
import * as PostLanguagesSettingsModal from './lang-settings/PostLanguagesSettings'
|
|
||||||
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
import * as UserAddRemoveLists from './UserAddRemoveLists'
|
||||||
|
|
||||||
export function ModalsContainer() {
|
export function ModalsContainer() {
|
||||||
@@ -59,8 +58,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||||||
element = <InviteCodesModal.Component />
|
element = <InviteCodesModal.Component />
|
||||||
} else if (modal.name === 'content-languages-settings') {
|
} else if (modal.name === 'content-languages-settings') {
|
||||||
element = <ContentLanguagesSettingsModal.Component />
|
element = <ContentLanguagesSettingsModal.Component />
|
||||||
} else if (modal.name === 'post-languages-settings') {
|
|
||||||
element = <PostLanguagesSettingsModal.Component />
|
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {StyleSheet, View} from 'react-native'
|
|
||||||
import {Trans} from '@lingui/macro'
|
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {deviceLanguageCodes} from '#/locale/deviceLocales'
|
|
||||||
import {languageName} from '#/locale/helpers'
|
|
||||||
import {useModalControls} from '#/state/modals'
|
|
||||||
import {
|
|
||||||
hasPostLanguage,
|
|
||||||
useLanguagePrefs,
|
|
||||||
useLanguagePrefsApi,
|
|
||||||
} from '#/state/preferences/languages'
|
|
||||||
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
|
||||||
import {LANGUAGES, LANGUAGES_MAP_CODE2} from '../../../../locale/languages'
|
|
||||||
import {Text} from '../../util/text/Text'
|
|
||||||
import {ScrollView} from '../util'
|
|
||||||
import {ConfirmLanguagesButton} from './ConfirmLanguagesButton'
|
|
||||||
|
|
||||||
export const snapPoints = ['100%']
|
|
||||||
|
|
||||||
export function Component() {
|
|
||||||
const {closeModal} = useModalControls()
|
|
||||||
const langPrefs = useLanguagePrefs()
|
|
||||||
const setLangPrefs = useLanguagePrefsApi()
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const onPressDone = React.useCallback(() => {
|
|
||||||
closeModal()
|
|
||||||
}, [closeModal])
|
|
||||||
|
|
||||||
const languages = React.useMemo(() => {
|
|
||||||
const langs = LANGUAGES.filter(
|
|
||||||
lang =>
|
|
||||||
!!lang.code2.trim() &&
|
|
||||||
LANGUAGES_MAP_CODE2[lang.code2].code3 === lang.code3,
|
|
||||||
)
|
|
||||||
// sort so that device & selected languages are on top, then alphabetically
|
|
||||||
langs.sort((a, b) => {
|
|
||||||
const hasA =
|
|
||||||
hasPostLanguage(langPrefs.postLanguage, a.code2) ||
|
|
||||||
deviceLanguageCodes.includes(a.code2)
|
|
||||||
const hasB =
|
|
||||||
hasPostLanguage(langPrefs.postLanguage, b.code2) ||
|
|
||||||
deviceLanguageCodes.includes(b.code2)
|
|
||||||
if (hasA === hasB) return a.name.localeCompare(b.name)
|
|
||||||
if (hasA) return -1
|
|
||||||
return 1
|
|
||||||
})
|
|
||||||
return langs
|
|
||||||
}, [langPrefs])
|
|
||||||
|
|
||||||
const onPress = React.useCallback(
|
|
||||||
(code2: string) => {
|
|
||||||
setLangPrefs.togglePostLanguage(code2)
|
|
||||||
},
|
|
||||||
[setLangPrefs],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
testID="postLanguagesModal"
|
|
||||||
style={[
|
|
||||||
pal.view,
|
|
||||||
styles.container,
|
|
||||||
// @ts-ignore vh is on web only
|
|
||||||
isMobile
|
|
||||||
? {
|
|
||||||
paddingTop: 20,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
maxHeight: '90vh',
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<Text style={[pal.text, styles.title]}>
|
|
||||||
<Trans>Post Languages</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[pal.text, styles.description]}>
|
|
||||||
<Trans>Which languages are used in this post?</Trans>
|
|
||||||
</Text>
|
|
||||||
<ScrollView style={styles.scrollContainer}>
|
|
||||||
{languages.map(lang => {
|
|
||||||
const isSelected = hasPostLanguage(langPrefs.postLanguage, lang.code2)
|
|
||||||
|
|
||||||
// enforce a max of 3 selections for post languages
|
|
||||||
let isDisabled = false
|
|
||||||
if (langPrefs.postLanguage.split(',').length >= 3 && !isSelected) {
|
|
||||||
isDisabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ToggleButton
|
|
||||||
key={lang.code2}
|
|
||||||
label={languageName(lang, langPrefs.appLanguage)}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onPress={() => (isDisabled ? undefined : onPress(lang.code2))}
|
|
||||||
style={[
|
|
||||||
pal.border,
|
|
||||||
styles.languageToggle,
|
|
||||||
isDisabled && styles.dimmed,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
height: isMobile ? 60 : 0,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
|
||||||
<ConfirmLanguagesButton onPress={onPressDone} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
textAlign: 'center',
|
|
||||||
fontWeight: '600',
|
|
||||||
fontSize: 24,
|
|
||||||
marginBottom: 12,
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
textAlign: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
marginBottom: 10,
|
|
||||||
},
|
|
||||||
scrollContainer: {
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
},
|
|
||||||
languageToggle: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderRadius: 0,
|
|
||||||
paddingHorizontal: 6,
|
|
||||||
paddingVertical: 12,
|
|
||||||
},
|
|
||||||
dimmed: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
import {type PropsWithChildren} from 'react'
|
|
||||||
import {useMemo, useRef} from 'react'
|
|
||||||
import {
|
|
||||||
Dimensions,
|
|
||||||
type GestureResponderEvent,
|
|
||||||
type Insets,
|
|
||||||
type StyleProp,
|
|
||||||
StyleSheet,
|
|
||||||
TouchableOpacity,
|
|
||||||
TouchableWithoutFeedback,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
type ViewStyle,
|
|
||||||
} from 'react-native'
|
|
||||||
import Animated, {FadeIn, FadeInDown, FadeInUp} from 'react-native-reanimated'
|
|
||||||
import RootSiblings from 'react-native-root-siblings'
|
|
||||||
import {type IconProp} from '@fortawesome/fontawesome-svg-core'
|
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg} from '@lingui/macro'
|
|
||||||
import {useLingui} from '@lingui/react'
|
|
||||||
import type React from 'react'
|
|
||||||
|
|
||||||
import {HITSLOP_10} from '#/lib/constants'
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
|
||||||
import {colors} from '#/lib/styles'
|
|
||||||
import {useTheme} from '#/lib/ThemeContext'
|
|
||||||
import {isWeb} from '#/platform/detection'
|
|
||||||
import {native} from '#/alf'
|
|
||||||
import {FullWindowOverlay} from '#/components/FullWindowOverlay'
|
|
||||||
import {Text} from '../text/Text'
|
|
||||||
import {Button, type ButtonType} from './Button'
|
|
||||||
|
|
||||||
const ESTIMATED_BTN_HEIGHT = 50
|
|
||||||
const ESTIMATED_SEP_HEIGHT = 16
|
|
||||||
const ESTIMATED_HEADING_HEIGHT = 60
|
|
||||||
|
|
||||||
export interface DropdownItemButton {
|
|
||||||
testID?: string
|
|
||||||
icon?: IconProp
|
|
||||||
label: string
|
|
||||||
onPress: () => void
|
|
||||||
}
|
|
||||||
export interface DropdownItemSeparator {
|
|
||||||
sep: true
|
|
||||||
}
|
|
||||||
export interface DropdownItemHeading {
|
|
||||||
heading: true
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
export type DropdownItem =
|
|
||||||
| DropdownItemButton
|
|
||||||
| DropdownItemSeparator
|
|
||||||
| DropdownItemHeading
|
|
||||||
type MaybeDropdownItem = DropdownItem | false | undefined
|
|
||||||
|
|
||||||
export type DropdownButtonType = ButtonType | 'bare'
|
|
||||||
|
|
||||||
interface DropdownButtonProps {
|
|
||||||
testID?: string
|
|
||||||
type?: DropdownButtonType
|
|
||||||
style?: StyleProp<ViewStyle>
|
|
||||||
items: MaybeDropdownItem[]
|
|
||||||
label?: string
|
|
||||||
menuWidth?: number
|
|
||||||
children?: React.ReactNode
|
|
||||||
openToRight?: boolean
|
|
||||||
openUpwards?: boolean
|
|
||||||
rightOffset?: number
|
|
||||||
bottomOffset?: number
|
|
||||||
hitSlop?: Insets
|
|
||||||
accessibilityLabel?: string
|
|
||||||
accessibilityHint?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
|
||||||
*/
|
|
||||||
export function DropdownButton({
|
|
||||||
testID,
|
|
||||||
type = 'bare',
|
|
||||||
style,
|
|
||||||
items,
|
|
||||||
label,
|
|
||||||
menuWidth,
|
|
||||||
children,
|
|
||||||
openToRight = false,
|
|
||||||
openUpwards = false,
|
|
||||||
rightOffset = 0,
|
|
||||||
bottomOffset = 0,
|
|
||||||
hitSlop = HITSLOP_10,
|
|
||||||
accessibilityLabel,
|
|
||||||
}: PropsWithChildren<DropdownButtonProps>) {
|
|
||||||
const {_} = useLingui()
|
|
||||||
|
|
||||||
const ref1 = useRef<View>(null)
|
|
||||||
const ref2 = useRef<View>(null)
|
|
||||||
|
|
||||||
const onPress = (e: GestureResponderEvent) => {
|
|
||||||
const ref = ref1.current || ref2.current
|
|
||||||
const {height: winHeight} = Dimensions.get('window')
|
|
||||||
const pressY = e.nativeEvent.pageY
|
|
||||||
ref?.measure(
|
|
||||||
(
|
|
||||||
_x: number,
|
|
||||||
_y: number,
|
|
||||||
width: number,
|
|
||||||
_height: number,
|
|
||||||
pageX: number,
|
|
||||||
pageY: number,
|
|
||||||
) => {
|
|
||||||
if (!menuWidth) {
|
|
||||||
menuWidth = 200
|
|
||||||
}
|
|
||||||
let estimatedMenuHeight = 0
|
|
||||||
for (const item of items) {
|
|
||||||
if (item && isSep(item)) {
|
|
||||||
estimatedMenuHeight += ESTIMATED_SEP_HEIGHT
|
|
||||||
} else if (item && isBtn(item)) {
|
|
||||||
estimatedMenuHeight += ESTIMATED_BTN_HEIGHT
|
|
||||||
} else if (item && isHeading(item)) {
|
|
||||||
estimatedMenuHeight += ESTIMATED_HEADING_HEIGHT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newX = openToRight
|
|
||||||
? pageX + width + rightOffset
|
|
||||||
: pageX + width - menuWidth
|
|
||||||
|
|
||||||
// Add a bit of additional room
|
|
||||||
let newY = pressY + bottomOffset + 20
|
|
||||||
if (openUpwards || newY + estimatedMenuHeight > winHeight) {
|
|
||||||
newY -= estimatedMenuHeight
|
|
||||||
}
|
|
||||||
createDropdownMenu(
|
|
||||||
newX,
|
|
||||||
newY,
|
|
||||||
pageY,
|
|
||||||
menuWidth,
|
|
||||||
items.filter(v => !!v) as DropdownItem[],
|
|
||||||
openUpwards,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const numItems = useMemo(
|
|
||||||
() =>
|
|
||||||
items.filter(item => {
|
|
||||||
if (item === undefined || item === false) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return isBtn(item)
|
|
||||||
}).length,
|
|
||||||
[items],
|
|
||||||
)
|
|
||||||
|
|
||||||
if (type === 'bare') {
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID={testID}
|
|
||||||
style={style}
|
|
||||||
onPress={onPress}
|
|
||||||
hitSlop={hitSlop}
|
|
||||||
ref={ref1}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={
|
|
||||||
accessibilityLabel || _(msg`Opens ${numItems} options`)
|
|
||||||
}
|
|
||||||
accessibilityHint="">
|
|
||||||
{children}
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View ref={ref2}>
|
|
||||||
<Button
|
|
||||||
type={type}
|
|
||||||
testID={testID}
|
|
||||||
onPress={onPress}
|
|
||||||
style={style}
|
|
||||||
label={label}>
|
|
||||||
{children}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDropdownMenu(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
pageY: number,
|
|
||||||
width: number,
|
|
||||||
items: DropdownItem[],
|
|
||||||
opensUpwards = false,
|
|
||||||
): RootSiblings {
|
|
||||||
const onPressItem = (index: number) => {
|
|
||||||
sibling.destroy()
|
|
||||||
const item = items[index]
|
|
||||||
if (isBtn(item)) {
|
|
||||||
item.onPress()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onOuterPress = () => sibling.destroy()
|
|
||||||
const sibling = new RootSiblings(
|
|
||||||
(
|
|
||||||
<DropdownItems
|
|
||||||
onOuterPress={onOuterPress}
|
|
||||||
x={x}
|
|
||||||
y={y}
|
|
||||||
pageY={pageY}
|
|
||||||
width={width}
|
|
||||||
items={items}
|
|
||||||
onPressItem={onPressItem}
|
|
||||||
openUpwards={opensUpwards}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return sibling
|
|
||||||
}
|
|
||||||
|
|
||||||
type DropDownItemProps = {
|
|
||||||
onOuterPress: () => void
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
pageY: number
|
|
||||||
width: number
|
|
||||||
items: DropdownItem[]
|
|
||||||
onPressItem: (index: number) => void
|
|
||||||
openUpwards: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const DropdownItems = ({
|
|
||||||
onOuterPress,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
pageY,
|
|
||||||
width,
|
|
||||||
items,
|
|
||||||
onPressItem,
|
|
||||||
openUpwards,
|
|
||||||
}: DropDownItemProps) => {
|
|
||||||
const pal = usePalette('default')
|
|
||||||
const theme = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {height: screenHeight} = useWindowDimensions()
|
|
||||||
const dropDownBackgroundColor =
|
|
||||||
theme.colorScheme === 'dark' ? pal.btn : pal.view
|
|
||||||
const separatorColor =
|
|
||||||
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
|
||||||
|
|
||||||
const numItems = items.filter(isBtn).length
|
|
||||||
|
|
||||||
// TODO: Refactor dropdown components to:
|
|
||||||
// - (On web, if not handled by React Native) use semantic <select />
|
|
||||||
// and <option /> elements for keyboard navigation out of the box
|
|
||||||
// - (On mobile) be buttons by default, accept `label` and `nativeID`
|
|
||||||
// props, and always have an explicit label
|
|
||||||
return (
|
|
||||||
<FullWindowOverlay>
|
|
||||||
{/* This TouchableWithoutFeedback renders the background so if the user clicks outside, the dropdown closes */}
|
|
||||||
<TouchableWithoutFeedback
|
|
||||||
onPress={onOuterPress}
|
|
||||||
accessibilityLabel={_(msg`Toggle dropdown`)}
|
|
||||||
accessibilityHint="">
|
|
||||||
<Animated.View
|
|
||||||
entering={FadeIn}
|
|
||||||
style={[
|
|
||||||
styles.bg,
|
|
||||||
// On web we need to adjust the top and bottom relative to the scroll position
|
|
||||||
isWeb
|
|
||||||
? {
|
|
||||||
top: -pageY,
|
|
||||||
bottom: pageY - screenHeight,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</TouchableWithoutFeedback>
|
|
||||||
<Animated.View
|
|
||||||
entering={native(
|
|
||||||
openUpwards ? FadeInDown.springify(1000) : FadeInUp.springify(1000),
|
|
||||||
)}
|
|
||||||
style={[
|
|
||||||
styles.menu,
|
|
||||||
{left: x, top: y, width},
|
|
||||||
dropDownBackgroundColor,
|
|
||||||
]}>
|
|
||||||
{items.map((item, index) => {
|
|
||||||
if (isBtn(item)) {
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
testID={item.testID}
|
|
||||||
key={index}
|
|
||||||
style={[styles.menuItem]}
|
|
||||||
onPress={() => onPressItem(index)}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel={item.label}
|
|
||||||
accessibilityHint={_(
|
|
||||||
msg`Selects option ${index + 1} of ${numItems}`,
|
|
||||||
)}>
|
|
||||||
{item.icon && (
|
|
||||||
<FontAwesomeIcon
|
|
||||||
style={styles.icon}
|
|
||||||
icon={item.icon}
|
|
||||||
color={pal.text.color as string}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text style={[styles.label, pal.text]}>{item.label}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
)
|
|
||||||
} else if (isSep(item)) {
|
|
||||||
return (
|
|
||||||
<View key={index} style={[styles.separator, separatorColor]} />
|
|
||||||
)
|
|
||||||
} else if (isHeading(item)) {
|
|
||||||
return (
|
|
||||||
<View style={[styles.heading, pal.border]} key={index}>
|
|
||||||
<Text style={[pal.text, styles.headingLabel]}>
|
|
||||||
{item.label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
})}
|
|
||||||
</Animated.View>
|
|
||||||
</FullWindowOverlay>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSep(item: DropdownItem): item is DropdownItemSeparator {
|
|
||||||
return 'sep' in item && item.sep
|
|
||||||
}
|
|
||||||
function isHeading(item: DropdownItem): item is DropdownItemHeading {
|
|
||||||
return 'heading' in item && item.heading
|
|
||||||
}
|
|
||||||
function isBtn(item: DropdownItem): item is DropdownItemButton {
|
|
||||||
return !isSep(item) && !isHeading(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
bg: {
|
|
||||||
position: 'absolute',
|
|
||||||
left: 0,
|
|
||||||
width: '100%',
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
||||||
},
|
|
||||||
menu: {
|
|
||||||
position: 'absolute',
|
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderRadius: 14,
|
|
||||||
paddingVertical: 6,
|
|
||||||
},
|
|
||||||
menuItem: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingLeft: 15,
|
|
||||||
paddingRight: 40,
|
|
||||||
},
|
|
||||||
menuItemBorder: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
borderTopColor: colors.gray1,
|
|
||||||
marginTop: 4,
|
|
||||||
paddingTop: 12,
|
|
||||||
},
|
|
||||||
icon: {
|
|
||||||
marginLeft: 2,
|
|
||||||
marginRight: 8,
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
fontSize: 18,
|
|
||||||
flexShrink: 1,
|
|
||||||
flexGrow: 1,
|
|
||||||
},
|
|
||||||
separator: {
|
|
||||||
borderTopWidth: 1,
|
|
||||||
marginVertical: 8,
|
|
||||||
},
|
|
||||||
heading: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'center',
|
|
||||||
paddingVertical: 10,
|
|
||||||
paddingLeft: 15,
|
|
||||||
paddingRight: 20,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
marginBottom: 6,
|
|
||||||
},
|
|
||||||
headingLabel: {
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -4,17 +4,16 @@ import {msg} from '@lingui/macro'
|
|||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
import {
|
||||||
|
type CommonNavigatorParams,
|
||||||
|
type NativeStackScreenProps,
|
||||||
|
} from '#/lib/routes/types'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {PaletteColorName, ThemeProvider} from '#/lib/ThemeContext'
|
import {type PaletteColorName, ThemeProvider} from '#/lib/ThemeContext'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
|
||||||
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
|
||||||
import {Button} from '#/view/com/util/forms/Button'
|
import {Button} from '#/view/com/util/forms/Button'
|
||||||
import {
|
|
||||||
DropdownButton,
|
|
||||||
DropdownItem,
|
|
||||||
} from '#/view/com/util/forms/DropdownButton'
|
|
||||||
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
import {ToggleButton} from '#/view/com/util/forms/ToggleButton'
|
||||||
import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder'
|
import * as LoadingPlaceholder from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
@@ -134,8 +133,6 @@ function ControlsView() {
|
|||||||
<ScrollView style={[s.pl10, s.pr10]}>
|
<ScrollView style={[s.pl10, s.pr10]}>
|
||||||
<Heading label="Buttons" />
|
<Heading label="Buttons" />
|
||||||
<ButtonsView />
|
<ButtonsView />
|
||||||
<Heading label="Dropdown Buttons" />
|
|
||||||
<DropdownButtonsView />
|
|
||||||
<Heading label="Toggle Buttons" />
|
<Heading label="Toggle Buttons" />
|
||||||
<ToggleButtonsView />
|
<ToggleButtonsView />
|
||||||
<View style={s.footerSpacer} />
|
<View style={s.footerSpacer} />
|
||||||
@@ -396,44 +393,6 @@ function ButtonsView() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const DROPDOWN_ITEMS: DropdownItem[] = [
|
|
||||||
{
|
|
||||||
icon: ['far', 'paste'],
|
|
||||||
label: 'Copy post text',
|
|
||||||
onPress() {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'share',
|
|
||||||
label: 'Share...',
|
|
||||||
onPress() {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'circle-exclamation',
|
|
||||||
label: 'Report post',
|
|
||||||
onPress() {},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
function DropdownButtonsView() {
|
|
||||||
const defaultPal = usePalette('default')
|
|
||||||
return (
|
|
||||||
<View style={[defaultPal.view]}>
|
|
||||||
<View style={s.mb5}>
|
|
||||||
<DropdownButton
|
|
||||||
type="primary"
|
|
||||||
items={DROPDOWN_ITEMS}
|
|
||||||
menuWidth={200}
|
|
||||||
label="Primary button"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={s.mb5}>
|
|
||||||
<DropdownButton type="bare" items={DROPDOWN_ITEMS} menuWidth={200}>
|
|
||||||
<Text>Bare</Text>
|
|
||||||
</DropdownButton>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ToggleButtonsView() {
|
function ToggleButtonsView() {
|
||||||
const defaultPal = usePalette('default')
|
const defaultPal = usePalette('default')
|
||||||
const buttonStyles = s.mb5
|
const buttonStyles = s.mb5
|
||||||
|
|||||||
Reference in New Issue
Block a user