bump
This commit is contained in:
@@ -30,7 +30,10 @@ const NativeView: React.ComponentType<
|
||||
|
||||
const NativeModule = requireNativeModule('BottomSheet')
|
||||
|
||||
const isIOS15 = Platform.OS === 'ios' && Number(Platform.Version) < 16
|
||||
const isIOS15 =
|
||||
Platform.OS === 'ios' &&
|
||||
// semvar - can be 3 segments, so can't use Number(Platform.Version)
|
||||
Number(Platform.Version.split('.').at(0)) < 16
|
||||
|
||||
export class BottomSheetNativeComponent extends React.Component<
|
||||
BottomSheetViewProps,
|
||||
|
||||
@@ -9,7 +9,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private var scrollView: UIScrollView?
|
||||
private var rctScrollView: RCTScrollView?
|
||||
private var rctRefreshCtrl: RCTRefreshControl?
|
||||
private var cancelGestureRecognizers: [UIGestureRecognizer]?
|
||||
private var animTimer: Timer?
|
||||
@@ -68,7 +68,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
@IBAction func callOnPan(_ sender: UIPanGestureRecognizer) {
|
||||
guard let sv = self.scrollView else {
|
||||
guard let rctsv = self.rctScrollView, let sv = rctsv.scrollView else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
func startDecayAnimation(_ translation: CGFloat, _ velocity: CGFloat) {
|
||||
guard let sv = self.scrollView else {
|
||||
guard let sv = self.rctScrollView?.scrollView else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -160,49 +160,32 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
|
||||
return offset
|
||||
}
|
||||
|
||||
private func findScrollView(in view: UIView, foundCount: Int) -> UIScrollView? {
|
||||
var foundCount = foundCount
|
||||
if let sv = view as? UIScrollView { return sv }
|
||||
for child in view.subviews {
|
||||
if let found = findScrollView(in: child, foundCount: foundCount) {
|
||||
if foundCount == 1 {
|
||||
print("found sv: \(found)")
|
||||
// return found
|
||||
} else {
|
||||
print("found sv: \(found)")
|
||||
foundCount += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tryFindScrollView() {
|
||||
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
|
||||
// Otherwise we might end up with duplicates when we switch back to that scrollview.
|
||||
self.removeCancelGestureRecognizers()
|
||||
|
||||
guard let sv = self.findScrollView(in: self.superview!.superview!.superview!, foundCount: 0) else {
|
||||
print("⚠️ ExpoScrollForwarder: couldn’t find UIScrollView under tag \(tag)")
|
||||
guard let scrollViewTag = scrollViewTag else {
|
||||
return
|
||||
}
|
||||
|
||||
self.scrollView = sv
|
||||
self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
|
||||
// Before we switch to a different scrollview, we always want to remove the cancel gesture recognizer.
|
||||
// Otherwise we might end up with duplicates when we switch back to that scrollview.
|
||||
self.removeCancelGestureRecognizers()
|
||||
|
||||
self.rctScrollView = self.appContext?
|
||||
.findView(withTag: scrollViewTag, ofType: RCTScrollView.self)
|
||||
self.rctRefreshCtrl = self.rctScrollView?.scrollView.refreshControl as? RCTRefreshControl
|
||||
|
||||
self.addCancelGestureRecognizers()
|
||||
}
|
||||
|
||||
func addCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
self.scrollView?.addGestureRecognizer(r)
|
||||
self.rctScrollView?.scrollView?.addGestureRecognizer(r)
|
||||
}
|
||||
}
|
||||
|
||||
func removeCancelGestureRecognizers() {
|
||||
self.cancelGestureRecognizers?.forEach { r in
|
||||
self.scrollView?.removeGestureRecognizer(r)
|
||||
self.rctScrollView?.scrollView?.removeGestureRecognizer(r)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +202,7 @@ class ExpoScrollForwarderView: ExpoView, UIGestureRecognizerDelegate {
|
||||
}
|
||||
|
||||
func scrollToOffset(_ offset: Int, animated: Bool = true) {
|
||||
self.scrollView?.scrollRectToVisible(CGRect(x: 0, y: offset, width: 0, height: 0), animated: animated)
|
||||
self.rctScrollView?.scroll(toOffset: CGPoint(x: 0, y: offset), animated: animated)
|
||||
}
|
||||
|
||||
func stopTimer() {
|
||||
|
||||
@@ -193,7 +193,6 @@
|
||||
"react-native-keyboard-controller": "^1.17.1",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.7.1",
|
||||
"react-native-picker-select": "^9.3.1",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
|
||||
@@ -204,6 +204,9 @@ export const atoms = {
|
||||
flex_grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
flex_grow_0: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
flex_shrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {APP_LANGUAGES} from '#/locale/languages'
|
||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {atoms as a, platform, useTheme} from '#/alf'
|
||||
import * as Select from '#/components/Select'
|
||||
import {Button} from './Button'
|
||||
|
||||
export function AppLanguageDropdown(_props: ViewStyleProp) {
|
||||
export function AppLanguageDropdown() {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
@@ -19,7 +21,7 @@ export function AppLanguageDropdown(_props: ViewStyleProp) {
|
||||
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
|
||||
|
||||
const onChangeAppLanguage = React.useCallback(
|
||||
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||
(value: string) => {
|
||||
if (!value) return
|
||||
if (sanitizedLang !== value) {
|
||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||
@@ -32,43 +34,48 @@ export function AppLanguageDropdown(_props: ViewStyleProp) {
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={a.relative}>
|
||||
<RNPickerSelect
|
||||
darkTheme={t.scheme === 'dark'}
|
||||
placeholder={{}}
|
||||
value={sanitizedLang}
|
||||
onValueChange={onChangeAppLanguage}
|
||||
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||
<Select.Root
|
||||
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
|
||||
onValueChange={onChangeAppLanguage}>
|
||||
<Select.Trigger label={_(msg`Change app language`)}>
|
||||
{({props}) => (
|
||||
<Button
|
||||
{...props}
|
||||
label={props.accessibilityLabel}
|
||||
size={platform({
|
||||
web: 'tiny',
|
||||
native: 'small',
|
||||
})}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
style={[
|
||||
a.pr_xs,
|
||||
a.pl_sm,
|
||||
platform({
|
||||
web: [{alignSelf: 'flex-start'}, a.gap_sm],
|
||||
native: [a.gap_xs],
|
||||
}),
|
||||
]}>
|
||||
<Select.ValueText
|
||||
placeholder={_(msg`Select an app language`)}
|
||||
style={[t.atoms.text_contrast_medium]}
|
||||
/>
|
||||
<Select.Icon style={[t.atoms.text_contrast_medium]} />
|
||||
</Button>
|
||||
)}
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
renderItem={({label, value}) => (
|
||||
<Select.Item value={value} label={label}>
|
||||
<Select.ItemIndicator />
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)}
|
||||
items={APP_LANGUAGES.map(l => ({
|
||||
label: l.name,
|
||||
value: l.code2,
|
||||
key: l.code2,
|
||||
}))}
|
||||
useNativeAndroidPickerStyle={false}
|
||||
style={{
|
||||
inputAndroid: {
|
||||
color: t.atoms.text_contrast_medium.color,
|
||||
fontSize: 16,
|
||||
paddingRight: 12 + 4,
|
||||
},
|
||||
inputIOS: {
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 16,
|
||||
paddingRight: 12 + 4,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{left: 'auto'},
|
||||
{pointerEvents: 'none'},
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
]}>
|
||||
<ChevronDown fill={t.atoms.text.color} size="xs" />
|
||||
</View>
|
||||
</View>
|
||||
</Select.Root>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {APP_LANGUAGES} from '#/locale/languages'
|
||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||
import {resetPostsFeedQueries} from '#/state/queries/post-feed'
|
||||
import {atoms as a, useTheme, ViewStyleProp} from '#/alf'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function AppLanguageDropdown({style}: ViewStyleProp) {
|
||||
const t = useTheme()
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
const langPrefs = useLanguagePrefs()
|
||||
const setLangPrefs = useLanguagePrefsApi()
|
||||
|
||||
const sanitizedLang = sanitizeAppLanguageSetting(langPrefs.appLanguage)
|
||||
|
||||
const onChangeAppLanguage = React.useCallback(
|
||||
(ev: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const value = ev.target.value
|
||||
|
||||
if (!value) return
|
||||
if (sanitizedLang !== value) {
|
||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||
}
|
||||
|
||||
// reset feeds to refetch content
|
||||
resetPostsFeedQueries(queryClient)
|
||||
},
|
||||
[sanitizedLang, setLangPrefs, queryClient],
|
||||
)
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
// We don't have hitSlop here to increase the tap region,
|
||||
// alternative is negative margins.
|
||||
{height: 32, marginVertical: -((32 - 14) / 2)},
|
||||
style,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
a.align_center,
|
||||
a.flex_shrink,
|
||||
a.h_full,
|
||||
t.atoms.bg,
|
||||
]}>
|
||||
<Text aria-hidden={true} style={t.atoms.text_contrast_medium}>
|
||||
{APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name}
|
||||
</Text>
|
||||
<ChevronDown fill={t.atoms.text.color} size="xs" style={a.flex_0} />
|
||||
</View>
|
||||
|
||||
<select
|
||||
value={sanitizedLang}
|
||||
onChange={onChangeAppLanguage}
|
||||
style={{
|
||||
fontSize: a.text_sm.fontSize,
|
||||
letterSpacing: a.text_sm.letterSpacing,
|
||||
cursor: 'pointer',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0,
|
||||
color: t.atoms.text.color,
|
||||
background: t.atoms.bg.backgroundColor,
|
||||
padding: 4,
|
||||
maxWidth: '100%',
|
||||
}}>
|
||||
{APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => (
|
||||
<option key={l.code2} value={l.code2}>
|
||||
{l.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import React from 'react'
|
||||
import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
type LayoutChangeEvent,
|
||||
type StyleProp,
|
||||
type TextStyle,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import type React from 'react'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
@@ -9,15 +15,18 @@ export function Header({
|
||||
renderRight,
|
||||
children,
|
||||
style,
|
||||
onLayout,
|
||||
}: {
|
||||
renderLeft?: () => React.ReactNode
|
||||
renderRight?: () => React.ReactNode
|
||||
children?: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
onLayout?: (event: LayoutChangeEvent) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
onLayout={onLayout}
|
||||
style={[
|
||||
a.relative,
|
||||
a.w_full,
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {ChevronTopBottom_Stroke2_Corner0_Rounded as ChevronUpDownIcon} from '#/components/icons/Chevron'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
type ContentProps,
|
||||
type IconProps,
|
||||
type ItemIndicatorProps,
|
||||
type ItemProps,
|
||||
type ItemTextProps,
|
||||
type RootProps,
|
||||
type TriggerProps,
|
||||
type ValueProps,
|
||||
} from './types'
|
||||
|
||||
type ContextType = {
|
||||
control: Dialog.DialogControlProps
|
||||
} & Pick<RootProps, 'value' | 'onValueChange' | 'disabled'>
|
||||
|
||||
const Context = createContext<ContextType | null>(null)
|
||||
|
||||
const ValueTextContext = createContext<
|
||||
[any, React.Dispatch<React.SetStateAction<any>>]
|
||||
>([undefined, () => {}])
|
||||
|
||||
function useSelectContext() {
|
||||
const ctx = useContext(Context)
|
||||
if (!ctx) {
|
||||
throw new Error('Select components must must be used within a Select.Root')
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
export function Root({children, value, onValueChange, disabled}: RootProps) {
|
||||
const control = Dialog.useDialogControl()
|
||||
const valueTextCtx = useState<any>()
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({
|
||||
control,
|
||||
value,
|
||||
onValueChange,
|
||||
disabled,
|
||||
}),
|
||||
[control, value, onValueChange, disabled],
|
||||
)
|
||||
return (
|
||||
<Context.Provider value={ctx}>
|
||||
<ValueTextContext.Provider value={valueTextCtx}>
|
||||
{children}
|
||||
</ValueTextContext.Provider>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function Trigger({children, label}: TriggerProps) {
|
||||
const {control} = useSelectContext()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const {
|
||||
state: pressed,
|
||||
onIn: onPressIn,
|
||||
onOut: onPressOut,
|
||||
} = useInteractionState()
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return children({
|
||||
isNative: true,
|
||||
control,
|
||||
state: {
|
||||
hovered: false,
|
||||
focused,
|
||||
pressed,
|
||||
},
|
||||
props: {
|
||||
onPress: control.open,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
accessibilityLabel: label,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
label={label}
|
||||
onPress={control.open}
|
||||
style={[a.flex_1, a.justify_between]}
|
||||
color="secondary"
|
||||
size="small"
|
||||
variant="solid">
|
||||
<>{children}</>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function ValueText({
|
||||
placeholder,
|
||||
children = value => value.label,
|
||||
style,
|
||||
}: ValueProps) {
|
||||
const [value] = useContext(ValueTextContext)
|
||||
const t = useTheme()
|
||||
|
||||
let text = value && children(value)
|
||||
if (typeof text !== 'string') text = placeholder
|
||||
|
||||
return (
|
||||
<ButtonText style={[t.atoms.text, a.font_normal, style]}>{text}</ButtonText>
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon({}: IconProps) {
|
||||
return <ButtonIcon icon={ChevronUpDownIcon} />
|
||||
}
|
||||
|
||||
export function Content<T>({
|
||||
items,
|
||||
valueExtractor = defaultItemValueExtractor,
|
||||
...props
|
||||
}: ContentProps<T>) {
|
||||
const {control, ...context} = useSelectContext()
|
||||
const [, setValue] = useContext(ValueTextContext)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const item = items.find(item => valueExtractor(item) === context.value)
|
||||
if (item) {
|
||||
setValue(item)
|
||||
}
|
||||
}, [items, context.value, valueExtractor, setValue])
|
||||
|
||||
return (
|
||||
<Dialog.Outer control={control}>
|
||||
<ContentInner
|
||||
control={control}
|
||||
items={items}
|
||||
valueExtractor={valueExtractor}
|
||||
{...props}
|
||||
{...context}
|
||||
/>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
function ContentInner<T>({
|
||||
items,
|
||||
renderItem,
|
||||
valueExtractor,
|
||||
...context
|
||||
}: ContentProps<T> & ContextType) {
|
||||
const control = Dialog.useDialogContext()
|
||||
|
||||
const {_} = useLingui()
|
||||
const [headerHeight, setHeaderHeight] = useState(50)
|
||||
|
||||
const render = useCallback(
|
||||
({item, index}: {item: T; index: number}) => {
|
||||
return renderItem(item, index, context.value)
|
||||
},
|
||||
[renderItem, context.value],
|
||||
)
|
||||
|
||||
const doneButton = useCallback(
|
||||
() => (
|
||||
<Button
|
||||
label={_(msg`Done`)}
|
||||
onPress={() => control.close()}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="ghost"
|
||||
style={[a.rounded_full]}>
|
||||
<ButtonText style={[a.text_md]}>
|
||||
<Trans>Done</Trans>
|
||||
</ButtonText>
|
||||
</Button>
|
||||
),
|
||||
[control, _],
|
||||
)
|
||||
|
||||
return (
|
||||
<Context.Provider value={context}>
|
||||
<Dialog.Header
|
||||
renderRight={doneButton}
|
||||
onLayout={evt => setHeaderHeight(evt.nativeEvent.layout.height)}
|
||||
style={[a.absolute, a.top_0, a.left_0, a.right_0, a.z_10]}>
|
||||
<Dialog.HeaderText>
|
||||
<Trans>Select an option</Trans>
|
||||
</Dialog.HeaderText>
|
||||
</Dialog.Header>
|
||||
<Dialog.InnerFlatList
|
||||
headerOffset={headerHeight}
|
||||
data={items}
|
||||
renderItem={render}
|
||||
keyExtractor={valueExtractor}
|
||||
/>
|
||||
</Context.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function defaultItemValueExtractor(item: any) {
|
||||
return item.value
|
||||
}
|
||||
|
||||
const ItemContext = createContext<{
|
||||
selected: boolean
|
||||
hovered: boolean
|
||||
focused: boolean
|
||||
pressed: boolean
|
||||
}>({
|
||||
selected: false,
|
||||
hovered: false,
|
||||
focused: false,
|
||||
pressed: false,
|
||||
})
|
||||
|
||||
export function useItemContext() {
|
||||
return useContext(ItemContext)
|
||||
}
|
||||
|
||||
export function Item({children, value, label, style}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const control = Dialog.useDialogContext()
|
||||
const {value: selected, onValueChange} = useSelectContext()
|
||||
|
||||
return (
|
||||
<Button
|
||||
role="listitem"
|
||||
label={label}
|
||||
style={[a.flex_1]}
|
||||
onPress={() => {
|
||||
control.close(() => {
|
||||
onValueChange?.(value)
|
||||
})
|
||||
}}>
|
||||
{({hovered, focused, pressed}) => (
|
||||
<ItemContext.Provider
|
||||
value={{selected: value === selected, hovered, focused, pressed}}>
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.pl_md,
|
||||
(focused || pressed) && t.atoms.bg_contrast_25,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</View>
|
||||
</ItemContext.Provider>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemText({children}: ItemTextProps) {
|
||||
const {selected} = useItemContext()
|
||||
const t = useTheme()
|
||||
|
||||
// eslint-disable-next-line bsky-internal/avoid-unwrapped-text
|
||||
return (
|
||||
<View style={[a.flex_1, a.py_md, a.border_b, t.atoms.border_contrast_low]}>
|
||||
<Text style={[a.text_md, selected && a.font_bold]}>{children}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
|
||||
const {selected} = useItemContext()
|
||||
|
||||
return <View style={{width: 24}}>{selected && <Icon size="md" />}</View>
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import {createContext, forwardRef, useContext, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {Select as RadixSelect} from 'radix-ui'
|
||||
|
||||
import {flatten, useTheme} from '#/alf'
|
||||
import {atoms as a} from '#/alf'
|
||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {
|
||||
ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon,
|
||||
ChevronTop_Stroke2_Corner0_Rounded as ChevronUpIcon,
|
||||
} from '#/components/icons/Chevron'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {
|
||||
type ContentProps,
|
||||
type IconProps,
|
||||
type ItemIndicatorProps,
|
||||
type ItemProps,
|
||||
type RadixPassThroughTriggerProps,
|
||||
type RootProps,
|
||||
type TriggerProps,
|
||||
type ValueProps,
|
||||
} from './types'
|
||||
|
||||
const SelectedValueContext = createContext<string | undefined | null>(null)
|
||||
|
||||
export function Root(props: RootProps) {
|
||||
return (
|
||||
<SelectedValueContext.Provider value={props.value}>
|
||||
<RadixSelect.Root {...props} />
|
||||
</SelectedValueContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const RadixTriggerPassThrough = forwardRef(
|
||||
(
|
||||
props: {
|
||||
children: (
|
||||
props: RadixPassThroughTriggerProps & {
|
||||
ref: React.Ref<any>
|
||||
},
|
||||
) => React.ReactNode
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// @ts-expect-error Radix provides no types of this stuff
|
||||
|
||||
return props.children?.({...props, ref})
|
||||
},
|
||||
)
|
||||
RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough'
|
||||
|
||||
export function Trigger({children, label}: TriggerProps) {
|
||||
const t = useTheme()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
onOut: onMouseLeave,
|
||||
} = useInteractionState()
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
|
||||
if (typeof children === 'function') {
|
||||
return (
|
||||
<RadixSelect.Trigger asChild>
|
||||
<RadixTriggerPassThrough>
|
||||
{props =>
|
||||
children({
|
||||
isNative: false,
|
||||
state: {
|
||||
hovered,
|
||||
focused,
|
||||
pressed: false,
|
||||
},
|
||||
props: {
|
||||
...props,
|
||||
onFocus: onFocus,
|
||||
onBlur: onBlur,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
accessibilityLabel: label,
|
||||
},
|
||||
})
|
||||
}
|
||||
</RadixTriggerPassThrough>
|
||||
</RadixSelect.Trigger>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<RadixSelect.Trigger
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
style={flatten([
|
||||
a.flex,
|
||||
a.relative,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_sm,
|
||||
a.w_full,
|
||||
{maxWidth: 400},
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.justify_between,
|
||||
a.py_sm,
|
||||
a.px_md,
|
||||
{
|
||||
outline: 0,
|
||||
borderWidth: 2,
|
||||
borderStyle: 'solid',
|
||||
borderColor: focused
|
||||
? t.palette.primary_500
|
||||
: hovered
|
||||
? t.palette.contrast_100
|
||||
: t.palette.contrast_25,
|
||||
},
|
||||
])}>
|
||||
{children}
|
||||
</RadixSelect.Trigger>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function ValueText({children: _, style, ...props}: ValueProps) {
|
||||
return (
|
||||
<Text style={style}>
|
||||
<RadixSelect.Value {...props} />
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
export function Icon({style}: IconProps) {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<RadixSelect.Icon>
|
||||
<ChevronDownIcon style={[t.atoms.text, style]} size="xs" />
|
||||
</RadixSelect.Icon>
|
||||
)
|
||||
}
|
||||
|
||||
export function Content<T>({items, renderItem}: ContentProps<T>) {
|
||||
const t = useTheme()
|
||||
const selectedValue = useContext(SelectedValueContext)
|
||||
|
||||
const scrollBtnStyles: React.CSSProperties[] = [
|
||||
a.absolute,
|
||||
a.flex,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
a.rounded_sm,
|
||||
a.z_10,
|
||||
]
|
||||
const up: React.CSSProperties[] = [
|
||||
...scrollBtnStyles,
|
||||
a.pt_sm,
|
||||
a.pb_lg,
|
||||
{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
background: `linear-gradient(to bottom, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
|
||||
},
|
||||
]
|
||||
const down: React.CSSProperties[] = [
|
||||
...scrollBtnStyles,
|
||||
a.pt_lg,
|
||||
a.pb_sm,
|
||||
{
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
background: `linear-gradient(to top, ${t.atoms.bg.backgroundColor} 0%, transparent 100%)`,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<RadixSelect.Portal>
|
||||
<RadixSelect.Content
|
||||
style={flatten([t.atoms.bg, a.rounded_sm, a.overflow_hidden])}
|
||||
position="popper"
|
||||
sideOffset={5}
|
||||
className="radix-select-content">
|
||||
<View
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.border,
|
||||
t.atoms.border_contrast_low,
|
||||
a.rounded_sm,
|
||||
]}>
|
||||
<RadixSelect.ScrollUpButton style={flatten(up)}>
|
||||
<ChevronUpIcon style={[t.atoms.text]} size="xs" />
|
||||
</RadixSelect.ScrollUpButton>
|
||||
<RadixSelect.Viewport style={flatten([a.p_xs])}>
|
||||
{items.map((item, index) => renderItem(item, index, selectedValue))}
|
||||
</RadixSelect.Viewport>
|
||||
<RadixSelect.ScrollDownButton style={flatten(down)}>
|
||||
<ChevronDownIcon style={[t.atoms.text]} size="xs" />
|
||||
</RadixSelect.ScrollDownButton>
|
||||
</View>
|
||||
</RadixSelect.Content>
|
||||
</RadixSelect.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
const ItemContext = createContext<{
|
||||
hovered: boolean
|
||||
focused: boolean
|
||||
pressed: boolean
|
||||
selected: boolean
|
||||
}>({
|
||||
hovered: false,
|
||||
focused: false,
|
||||
pressed: false,
|
||||
selected: false,
|
||||
})
|
||||
|
||||
export function useItemContext() {
|
||||
return useContext(ItemContext)
|
||||
}
|
||||
|
||||
export function Item({ref, value, style, children}: ItemProps) {
|
||||
const t = useTheme()
|
||||
const {
|
||||
state: hovered,
|
||||
onIn: onMouseEnter,
|
||||
onOut: onMouseLeave,
|
||||
} = useInteractionState()
|
||||
const selected = useContext(SelectedValueContext) === value
|
||||
const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState()
|
||||
const ctx = useMemo(
|
||||
() => ({hovered, focused, pressed: false, selected}),
|
||||
[hovered, focused, selected],
|
||||
)
|
||||
return (
|
||||
<RadixSelect.Item
|
||||
ref={ref}
|
||||
value={value}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
style={flatten([
|
||||
a.relative,
|
||||
a.flex,
|
||||
{minHeight: 25, paddingLeft: 30, paddingRight: 35},
|
||||
a.user_select_none,
|
||||
a.align_center,
|
||||
a.rounded_xs,
|
||||
a.py_2xs,
|
||||
a.text_sm,
|
||||
{outline: 0},
|
||||
(hovered || focused) && {backgroundColor: t.palette.primary_50},
|
||||
selected && [a.font_bold],
|
||||
a.transition_color,
|
||||
style,
|
||||
])}>
|
||||
<ItemContext.Provider value={ctx}>{children}</ItemContext.Provider>
|
||||
</RadixSelect.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export const ItemText = RadixSelect.ItemText
|
||||
|
||||
export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) {
|
||||
return (
|
||||
<RadixSelect.ItemIndicator
|
||||
style={flatten([
|
||||
a.absolute,
|
||||
{left: 0, width: 30},
|
||||
a.flex,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
])}>
|
||||
<Icon size="sm" />
|
||||
</RadixSelect.ItemIndicator>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import {
|
||||
type AccessibilityProps,
|
||||
type StyleProp,
|
||||
type TextStyle,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
|
||||
import {type TextStyleProp} from '#/alf'
|
||||
import {type DialogControlProps} from '#/components/Dialog'
|
||||
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||
|
||||
export type RootProps = {
|
||||
children?: React.ReactNode
|
||||
value?: string
|
||||
onValueChange?: (value: string) => void
|
||||
disabled?: boolean
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
defaultValue?: string
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
open?: boolean
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
defaultOpen?: boolean
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
onOpenChange?(open: boolean): void
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
autoComplete?: string
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
required?: boolean
|
||||
}
|
||||
|
||||
export type RadixPassThroughTriggerProps = {
|
||||
id: string
|
||||
type: 'button'
|
||||
disabled: boolean
|
||||
['data-disabled']: boolean
|
||||
['data-state']: string
|
||||
['aria-controls']?: string
|
||||
['aria-haspopup']?: boolean
|
||||
['aria-expanded']?: AccessibilityProps['aria-expanded']
|
||||
onPress: () => void
|
||||
}
|
||||
|
||||
export type TriggerProps = {
|
||||
children: React.ReactNode | ((props: TriggerChildProps) => React.ReactNode)
|
||||
label: string
|
||||
}
|
||||
|
||||
export type TriggerChildProps =
|
||||
| {
|
||||
isNative: true
|
||||
control: DialogControlProps
|
||||
state: {
|
||||
/**
|
||||
* Web only, `false` on native
|
||||
*/
|
||||
hovered: false
|
||||
focused: boolean
|
||||
pressed: boolean
|
||||
}
|
||||
/**
|
||||
* We don't necessarily know what these will be spread on to, so we
|
||||
* should add props one-by-one.
|
||||
*
|
||||
* On web, these properties are applied to a parent `Pressable`, so this
|
||||
* object is empty.
|
||||
*/
|
||||
props: {
|
||||
onPress: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
onPressIn: () => void
|
||||
onPressOut: () => void
|
||||
accessibilityLabel: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
isNative: false
|
||||
state: {
|
||||
hovered: boolean
|
||||
focused: boolean
|
||||
/**
|
||||
* Native only, `false` on web
|
||||
*/
|
||||
pressed: false
|
||||
}
|
||||
props: RadixPassThroughTriggerProps & {
|
||||
onPress: () => void
|
||||
onFocus: () => void
|
||||
onBlur: () => void
|
||||
onMouseEnter: () => void
|
||||
onMouseLeave: () => void
|
||||
accessibilityLabel: string
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* For use within the `Select.Trigger` component.
|
||||
* Shows the currently selected value. You can also
|
||||
* provide a placeholder to show when no value is selected.
|
||||
*
|
||||
* If you're passing items of a different shape than {value: string, label: string},
|
||||
* you'll need to pass a function to `children` that extracts the label from an item.
|
||||
*/
|
||||
export type ValueProps = {
|
||||
/**
|
||||
* Only needed for native. Extracts the label from an item. Defaults to `item => item.label`
|
||||
*/
|
||||
children?: (value: any) => string
|
||||
placeholder?: string
|
||||
style?: StyleProp<TextStyle>
|
||||
}
|
||||
|
||||
/*
|
||||
* Icon for use within the `Select.Trigger` component.
|
||||
* Changes based on platform - chevron down on web, up/down chevrons on native
|
||||
*
|
||||
* `style` prop is web only
|
||||
*/
|
||||
export type IconProps = TextStyleProp
|
||||
|
||||
export type ContentProps<T> = {
|
||||
/**
|
||||
* Items to render. Recommended to be in the form {value: string, label: string} - if not,
|
||||
* you need to provide a `valueExtractor` function to extract the value from an item and
|
||||
* customise the `Select.ValueText` component.
|
||||
*/
|
||||
items: T[]
|
||||
/**
|
||||
* Renders an item. You should probably use the `Select.Item` component.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* renderItem={({label, value}) => (
|
||||
* <Select.Item value={value} label={label}>
|
||||
* <Select.ItemIndicator />
|
||||
* <Select.ItemText>{label}</Select.ItemText>
|
||||
* </Select.Item>
|
||||
* )}
|
||||
* ```
|
||||
*/
|
||||
renderItem: (
|
||||
item: T,
|
||||
index: number,
|
||||
selectedValue?: string | null,
|
||||
) => React.ReactElement
|
||||
/*
|
||||
* Extracts the value from an item. Defaults to `item => item.value`
|
||||
*/
|
||||
valueExtractor?: (item: T) => string
|
||||
}
|
||||
|
||||
/*
|
||||
* An item within the select dropdown
|
||||
*/
|
||||
export type ItemProps = {
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
value: string
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
style?: StyleProp<ViewStyle>
|
||||
}
|
||||
|
||||
export type ItemTextProps = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export type ItemIndicatorProps = {
|
||||
icon?: React.ComponentType<SVGIconProps>
|
||||
}
|
||||
@@ -74,8 +74,8 @@ msgstr ""
|
||||
msgid "{0, plural, one {# second} other {# seconds}}"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:209
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:241
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:213
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:245
|
||||
#: src/view/shell/Drawer.tsx:470
|
||||
msgid "{0, plural, one {# unread item} other {# unread items}}"
|
||||
msgstr ""
|
||||
@@ -446,7 +446,7 @@ msgid "A new form of verification"
|
||||
msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:401
|
||||
#: src/screens/Settings/AboutSettings.tsx:72
|
||||
#: src/screens/Settings/AboutSettings.tsx:75
|
||||
#: src/screens/Settings/Settings.tsx:224
|
||||
#: src/screens/Settings/Settings.tsx:227
|
||||
msgid "About"
|
||||
@@ -621,7 +621,7 @@ msgstr ""
|
||||
msgid "Add muted words and tags"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1306
|
||||
#: src/view/com/composer/Composer.tsx:1308
|
||||
msgid "Add new post"
|
||||
msgstr ""
|
||||
|
||||
@@ -822,7 +822,7 @@ msgstr ""
|
||||
msgid "An error occurred while fetching the feed."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:336
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:337
|
||||
msgid "An error occurred while generating your starter pack. Want to try again?"
|
||||
msgstr ""
|
||||
|
||||
@@ -1119,7 +1119,7 @@ msgstr ""
|
||||
msgid "Before creating a post, you must first verify your email."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:343
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:344
|
||||
msgid "Before creating a starter pack, you must first verify your email."
|
||||
msgstr ""
|
||||
|
||||
@@ -1262,7 +1262,7 @@ msgstr ""
|
||||
msgid "Bluesky Social Terms of Service"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:303
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:304
|
||||
msgid "Bluesky will choose a set of recommended accounts from people in your network."
|
||||
msgstr ""
|
||||
|
||||
@@ -1519,7 +1519,7 @@ msgid "Changes hosting provider"
|
||||
msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:426
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:205
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:209
|
||||
#: src/view/shell/desktop/LeftNav.tsx:535
|
||||
#: src/view/shell/Drawer.tsx:438
|
||||
msgid "Chat"
|
||||
@@ -1587,7 +1587,7 @@ msgstr ""
|
||||
msgid "Choose Feeds"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:311
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:312
|
||||
msgid "Choose for me"
|
||||
msgstr ""
|
||||
|
||||
@@ -1627,8 +1627,8 @@ msgstr ""
|
||||
msgid "Clear all storage data (restart after this)"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:113
|
||||
#: src/screens/Settings/AboutSettings.tsx:117
|
||||
#: src/screens/Settings/AboutSettings.tsx:116
|
||||
#: src/screens/Settings/AboutSettings.tsx:120
|
||||
msgid "Clear image cache"
|
||||
msgstr ""
|
||||
|
||||
@@ -1807,7 +1807,7 @@ msgstr ""
|
||||
msgid "Compose reply"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1688
|
||||
#: src/view/com/composer/Composer.tsx:1690
|
||||
msgid "Compressing video..."
|
||||
msgstr ""
|
||||
|
||||
@@ -1958,7 +1958,7 @@ msgstr ""
|
||||
msgid "Copied"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:148
|
||||
#: src/screens/Settings/AboutSettings.tsx:151
|
||||
msgid "Copied build version to clipboard"
|
||||
msgstr ""
|
||||
|
||||
@@ -1976,7 +1976,7 @@ msgstr ""
|
||||
msgid "Copied!"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:124
|
||||
#: src/screens/Settings/AboutSettings.tsx:127
|
||||
msgid "Copies build version to clipboard"
|
||||
msgstr ""
|
||||
|
||||
@@ -2079,7 +2079,7 @@ msgstr ""
|
||||
msgid "Could not process your video"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:293
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:294
|
||||
msgid "Create"
|
||||
msgstr ""
|
||||
|
||||
@@ -2087,20 +2087,20 @@ msgstr ""
|
||||
msgid "Create a QR code for a starter pack"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:178
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:274
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:179
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:275
|
||||
#: src/Navigation.tsx:461
|
||||
msgid "Create a starter pack"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:255
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:256
|
||||
msgid "Create a starter pack for me"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/auth/SplashScreen.tsx:55
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:117
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:315
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:320
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:319
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:324
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:199
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:204
|
||||
#: src/view/shell/NavSignupCard.tsx:47
|
||||
@@ -2126,7 +2126,7 @@ msgstr ""
|
||||
msgid "Create an avatar instead"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:185
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:186
|
||||
msgid "Create another"
|
||||
msgstr ""
|
||||
|
||||
@@ -2350,12 +2350,12 @@ msgstr ""
|
||||
msgid "Detach quote post?"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:137
|
||||
#: src/screens/Settings/AboutSettings.tsx:140
|
||||
msgctxt "toast"
|
||||
msgid "Developer mode disabled"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:131
|
||||
#: src/screens/Settings/AboutSettings.tsx:134
|
||||
msgctxt "toast"
|
||||
msgid "Developer mode enabled"
|
||||
msgstr ""
|
||||
@@ -2435,7 +2435,7 @@ msgstr ""
|
||||
msgid "Dismiss"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1612
|
||||
#: src/view/com/composer/Composer.tsx:1614
|
||||
msgid "Dismiss error"
|
||||
msgstr ""
|
||||
|
||||
@@ -2886,7 +2886,7 @@ msgstr ""
|
||||
msgid "Entertainment"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1697
|
||||
#: src/view/com/composer/Composer.tsx:1699
|
||||
#: src/view/com/util/error/ErrorScreen.tsx:42
|
||||
msgid "Error"
|
||||
msgstr ""
|
||||
@@ -3494,7 +3494,7 @@ msgstr ""
|
||||
msgid "Gallery"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:300
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:301
|
||||
msgid "Generate a starter pack"
|
||||
msgstr ""
|
||||
|
||||
@@ -3780,7 +3780,7 @@ msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:645
|
||||
#: src/Navigation.tsx:665
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:162
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:166
|
||||
#: src/view/shell/desktop/LeftNav.tsx:599
|
||||
#: src/view/shell/Drawer.tsx:412
|
||||
msgid "Home"
|
||||
@@ -3871,12 +3871,12 @@ msgstr ""
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:61
|
||||
#: src/screens/Settings/AboutSettings.tsx:64
|
||||
msgid "Image cache cleared"
|
||||
msgstr ""
|
||||
|
||||
#. Android-only toast message which includes amount of space freed using localized number formatting
|
||||
#: src/screens/Settings/AboutSettings.tsx:47
|
||||
#: src/screens/Settings/AboutSettings.tsx:50
|
||||
msgid "Image cache cleared, freed {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -3997,7 +3997,7 @@ msgstr ""
|
||||
msgid "It's just you right now! Add more people to your starter pack by searching above."
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1631
|
||||
#: src/view/com/composer/Composer.tsx:1633
|
||||
msgid "Job ID: {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -4158,7 +4158,7 @@ msgstr ""
|
||||
msgid "left to go."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:316
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:317
|
||||
msgid "Let me choose"
|
||||
msgstr ""
|
||||
|
||||
@@ -4374,7 +4374,7 @@ msgstr ""
|
||||
msgid "Looks like you're missing a following feed. <0>Click here to add one.</0>"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:269
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:270
|
||||
msgid "Make one for me"
|
||||
msgstr ""
|
||||
|
||||
@@ -5039,7 +5039,7 @@ msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:655
|
||||
#: src/view/screens/Notifications.tsx:128
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:236
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:240
|
||||
#: src/view/shell/desktop/LeftNav.tsx:636
|
||||
#: src/view/shell/Drawer.tsx:465
|
||||
msgid "Notifications"
|
||||
@@ -5141,8 +5141,8 @@ msgid "Oops, something went wrong!"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/Lists.tsx:173
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:325
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:334
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:326
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:335
|
||||
#: src/screens/Settings/AppPasswords.tsx:59
|
||||
#: src/screens/Settings/components/ChangeHandleDialog.tsx:106
|
||||
#: src/screens/Settings/NotificationSettings.tsx:54
|
||||
@@ -5172,7 +5172,7 @@ msgid "Open drawer menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Messages/components/MessageInput.web.tsx:181
|
||||
#: src/view/com/composer/Composer.tsx:1292
|
||||
#: src/view/com/composer/Composer.tsx:1293
|
||||
msgid "Open emoji picker"
|
||||
msgstr ""
|
||||
|
||||
@@ -5185,7 +5185,7 @@ msgstr ""
|
||||
msgid "Open feed options menu"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/dms/EmojiPopup.android.tsx:28
|
||||
#: src/components/dms/EmojiPopup.android.tsx:27
|
||||
msgid "Open full emoji list"
|
||||
msgstr ""
|
||||
|
||||
@@ -5266,7 +5266,7 @@ msgstr ""
|
||||
msgid "Opens device photo gallery"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1293
|
||||
#: src/view/com/composer/Composer.tsx:1294
|
||||
msgid "Opens emoji picker"
|
||||
msgstr ""
|
||||
|
||||
@@ -5774,15 +5774,15 @@ msgid "Privacy and Security"
|
||||
msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:301
|
||||
#: src/screens/Settings/AboutSettings.tsx:89
|
||||
#: src/screens/Settings/AboutSettings.tsx:92
|
||||
#: src/screens/Settings/AboutSettings.tsx:95
|
||||
#: src/view/screens/PrivacyPolicy.tsx:31
|
||||
#: src/view/shell/Drawer.tsx:650
|
||||
#: src/view/shell/Drawer.tsx:651
|
||||
msgid "Privacy Policy"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1694
|
||||
#: src/view/com/composer/Composer.tsx:1696
|
||||
msgid "Processing video..."
|
||||
msgstr ""
|
||||
|
||||
@@ -5796,7 +5796,7 @@ msgstr ""
|
||||
msgid "profile"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:286
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:290
|
||||
#: src/view/shell/desktop/LeftNav.tsx:691
|
||||
#: src/view/shell/Drawer.tsx:74
|
||||
#: src/view/shell/Drawer.tsx:542
|
||||
@@ -6412,7 +6412,7 @@ msgstr ""
|
||||
#: src/components/Error.tsx:65
|
||||
#: src/components/Lists.tsx:110
|
||||
#: src/components/moderation/ReportDialog/index.tsx:221
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:339
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:340
|
||||
#: src/screens/Login/LoginForm.tsx:323
|
||||
#: src/screens/Login/LoginForm.tsx:330
|
||||
#: src/screens/Messages/ChatList.tsx:253
|
||||
@@ -6473,7 +6473,7 @@ msgstr ""
|
||||
msgid "Save"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/lightbox/ImageViewing/index.tsx:608
|
||||
#: src/view/com/lightbox/ImageViewing/index.tsx:610
|
||||
#: src/view/com/modals/CreateOrEditList.tsx:325
|
||||
msgctxt "action"
|
||||
msgid "Save"
|
||||
@@ -6556,7 +6556,7 @@ msgstr ""
|
||||
#: src/components/forms/SearchInput.tsx:36
|
||||
#: src/screens/Search/Shell.tsx:307
|
||||
#: src/screens/Search/Shell.tsx:464
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:182
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:186
|
||||
msgid "Search"
|
||||
msgstr ""
|
||||
|
||||
@@ -6884,7 +6884,7 @@ msgstr ""
|
||||
msgid "Share"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/lightbox/ImageViewing/index.tsx:617
|
||||
#: src/view/com/lightbox/ImageViewing/index.tsx:619
|
||||
msgctxt "action"
|
||||
msgid "Share"
|
||||
msgstr ""
|
||||
@@ -7073,8 +7073,8 @@ msgstr ""
|
||||
#: src/view/com/auth/SplashScreen.tsx:69
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:123
|
||||
#: src/view/com/auth/SplashScreen.web.tsx:131
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:325
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:330
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:329
|
||||
#: src/view/shell/bottom-bar/BottomBar.tsx:334
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:209
|
||||
#: src/view/shell/bottom-bar/BottomBarWeb.tsx:214
|
||||
#: src/view/shell/NavSignupCard.tsx:57
|
||||
@@ -7217,7 +7217,7 @@ msgstr ""
|
||||
msgid "Something wrong? Let us know."
|
||||
msgstr ""
|
||||
|
||||
#: src/App.native.tsx:122
|
||||
#: src/App.native.tsx:121
|
||||
#: src/App.web.tsx:98
|
||||
msgid "Sorry! Your session expired. Please sign in again."
|
||||
msgstr ""
|
||||
@@ -7303,12 +7303,12 @@ msgstr ""
|
||||
msgid "Starter Packs"
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:247
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:248
|
||||
msgid "Starter packs let you easily share your favorite feeds and people with your friends."
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:97
|
||||
#: src/screens/Settings/AboutSettings.tsx:100
|
||||
#: src/screens/Settings/AboutSettings.tsx:103
|
||||
msgid "Status Page"
|
||||
msgstr ""
|
||||
|
||||
@@ -7430,8 +7430,8 @@ msgstr ""
|
||||
msgid "System"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:104
|
||||
#: src/screens/Settings/AboutSettings.tsx:107
|
||||
#: src/screens/Settings/AboutSettings.tsx:110
|
||||
#: src/screens/Settings/Settings.tsx:380
|
||||
msgid "System log"
|
||||
msgstr ""
|
||||
@@ -7485,8 +7485,8 @@ msgid "Terms"
|
||||
msgstr ""
|
||||
|
||||
#: src/Navigation.tsx:306
|
||||
#: src/screens/Settings/AboutSettings.tsx:81
|
||||
#: src/screens/Settings/AboutSettings.tsx:84
|
||||
#: src/screens/Settings/AboutSettings.tsx:87
|
||||
#: src/view/screens/TermsOfService.tsx:31
|
||||
#: src/view/shell/Drawer.tsx:643
|
||||
#: src/view/shell/Drawer.tsx:645
|
||||
@@ -8369,7 +8369,7 @@ msgstr ""
|
||||
msgid "Uploading link thumbnail..."
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1691
|
||||
#: src/view/com/composer/Composer.tsx:1693
|
||||
msgid "Uploading video..."
|
||||
msgstr ""
|
||||
|
||||
@@ -8555,8 +8555,8 @@ msgstr ""
|
||||
msgid "Verify Your Email"
|
||||
msgstr ""
|
||||
|
||||
#: src/screens/Settings/AboutSettings.tsx:123
|
||||
#: src/screens/Settings/AboutSettings.tsx:152
|
||||
#: src/screens/Settings/AboutSettings.tsx:126
|
||||
#: src/screens/Settings/AboutSettings.tsx:155
|
||||
msgid "Version {appVersion}"
|
||||
msgstr ""
|
||||
|
||||
@@ -8599,7 +8599,7 @@ msgstr ""
|
||||
msgid "Video settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/composer/Composer.tsx:1701
|
||||
#: src/view/com/composer/Composer.tsx:1703
|
||||
msgid "Video uploaded"
|
||||
msgstr ""
|
||||
|
||||
@@ -8655,7 +8655,7 @@ msgstr ""
|
||||
msgid "View details for reporting a copyright violation"
|
||||
msgstr ""
|
||||
|
||||
#: src/view/com/posts/ViewFullThread.tsx:56
|
||||
#: src/view/com/posts/ViewFullThread.tsx:59
|
||||
msgid "View full thread"
|
||||
msgstr ""
|
||||
|
||||
@@ -9162,7 +9162,7 @@ msgstr ""
|
||||
msgid "You have temporarily reached the limit for video uploads. Please try again later."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:244
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:245
|
||||
msgid "You haven't created a starter pack yet!"
|
||||
msgstr ""
|
||||
|
||||
@@ -9199,7 +9199,7 @@ msgstr ""
|
||||
msgid "You must be 13 years of age or older to create an account."
|
||||
msgstr ""
|
||||
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:327
|
||||
#: src/components/StarterPack/ProfileStarterPacks.tsx:328
|
||||
msgid "You must be following at least seven other people to generate a starter pack."
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -1,23 +1,30 @@
|
||||
import {useCallback, useMemo} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import RNPickerSelect, {PickerSelectProps} from 'react-native-picker-select'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {APP_LANGUAGES, LANGUAGES} from '#/lib/../locale/languages'
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {
|
||||
type CommonNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
} from '#/lib/routes/types'
|
||||
import {languageName, sanitizeAppLanguageSetting} from '#/locale/helpers'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useLanguagePrefs, useLanguagePrefsApi} from '#/state/preferences'
|
||||
import {atoms as a, useTheme, web} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDownIcon} from '#/components/icons/Chevron'
|
||||
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
|
||||
import * as Layout from '#/components/Layout'
|
||||
import * as Select from '#/components/Select'
|
||||
import {Text} from '#/components/Typography'
|
||||
import * as SettingsList from './components/SettingsList'
|
||||
|
||||
const DEDUPED_LANGUAGES = LANGUAGES.filter(
|
||||
(lang, i, arr) =>
|
||||
lang.code2 && arr.findIndex(l => l.code2 === lang.code2) === i,
|
||||
)
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'LanguageSettings'>
|
||||
export function LanguageSettingsScreen({}: Props) {
|
||||
const {_} = useLingui()
|
||||
@@ -32,7 +39,7 @@ export function LanguageSettingsScreen({}: Props) {
|
||||
}, [openModal])
|
||||
|
||||
const onChangePrimaryLanguage = useCallback(
|
||||
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||
(value: string) => {
|
||||
if (!value) return
|
||||
if (langPrefs.primaryLanguage !== value) {
|
||||
setLangPrefs.setPrimaryLanguage(value)
|
||||
@@ -42,7 +49,7 @@ export function LanguageSettingsScreen({}: Props) {
|
||||
)
|
||||
|
||||
const onChangeAppLanguage = useCallback(
|
||||
(value: Parameters<PickerSelectProps['onValueChange']>[0]) => {
|
||||
(value: string) => {
|
||||
if (!value) return
|
||||
if (langPrefs.appLanguage !== value) {
|
||||
setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value))
|
||||
@@ -85,79 +92,26 @@ export function LanguageSettingsScreen({}: Props) {
|
||||
Select which language to use for the app's user interface.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.relative, web([a.w_full, {maxWidth: 400}])]}>
|
||||
<RNPickerSelect
|
||||
darkTheme={t.scheme === 'dark'}
|
||||
placeholder={{}}
|
||||
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
|
||||
onValueChange={onChangeAppLanguage}
|
||||
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||
<Select.Root
|
||||
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
|
||||
onValueChange={onChangeAppLanguage}>
|
||||
<Select.Trigger label={_(msg`Select app language`)}>
|
||||
<Select.ValueText />
|
||||
<Select.Icon />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
renderItem={({label, value}) => (
|
||||
<Select.Item value={value} label={label}>
|
||||
<Select.ItemIndicator />
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)}
|
||||
items={APP_LANGUAGES.map(l => ({
|
||||
label: l.name,
|
||||
value: l.code2,
|
||||
key: l.code2,
|
||||
}))}
|
||||
style={{
|
||||
inputAndroid: {
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
inputIOS: {
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
inputWeb: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
// @ts-ignore web only
|
||||
'-moz-appearance': 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
outline: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
fontFamily: 'inherit',
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.rounded_xs,
|
||||
a.pointer_events_none,
|
||||
a.align_center,
|
||||
a.justify_center,
|
||||
{
|
||||
top: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
width: 40,
|
||||
},
|
||||
]}>
|
||||
<ChevronDownIcon style={[t.atoms.text]} />
|
||||
</View>
|
||||
</View>
|
||||
</Select.Root>
|
||||
</View>
|
||||
</SettingsList.Group>
|
||||
<SettingsList.Divider />
|
||||
@@ -171,77 +125,26 @@ export function LanguageSettingsScreen({}: Props) {
|
||||
Select your preferred language for translations in your feed.
|
||||
</Trans>
|
||||
</Text>
|
||||
<View style={[a.relative, web([a.w_full, {maxWidth: 400}])]}>
|
||||
<RNPickerSelect
|
||||
darkTheme={t.scheme === 'dark'}
|
||||
placeholder={{}}
|
||||
value={langPrefs.primaryLanguage}
|
||||
onValueChange={onChangePrimaryLanguage}
|
||||
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
|
||||
<Select.Root
|
||||
value={langPrefs.primaryLanguage}
|
||||
onValueChange={onChangePrimaryLanguage}>
|
||||
<Select.Trigger label={_(msg`Select primary language`)}>
|
||||
<Select.ValueText />
|
||||
<Select.Icon />
|
||||
</Select.Trigger>
|
||||
<Select.Content
|
||||
renderItem={({label, value}) => (
|
||||
<Select.Item value={value} label={label}>
|
||||
<Select.ItemIndicator />
|
||||
<Select.ItemText>{label}</Select.ItemText>
|
||||
</Select.Item>
|
||||
)}
|
||||
items={DEDUPED_LANGUAGES.map(l => ({
|
||||
label: languageName(l, langPrefs.appLanguage),
|
||||
value: l.code2,
|
||||
key: l.code2 + l.code3,
|
||||
}))}
|
||||
style={{
|
||||
inputAndroid: {
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
inputIOS: {
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
inputWeb: {
|
||||
flex: 1,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
// @ts-ignore web only
|
||||
'-moz-appearance': 'none',
|
||||
'-webkit-appearance': 'none',
|
||||
appearance: 'none',
|
||||
outline: 0,
|
||||
borderWidth: 0,
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
color: t.atoms.text.color,
|
||||
fontSize: 14,
|
||||
fontFamily: 'inherit',
|
||||
letterSpacing: 0.5,
|
||||
fontWeight: a.font_bold.fontWeight,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 1,
|
||||
right: 1,
|
||||
bottom: 1,
|
||||
width: 40,
|
||||
backgroundColor: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderRadius: a.rounded_xs.borderRadius,
|
||||
pointerEvents: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}>
|
||||
<ChevronDownIcon style={t.atoms.text} />
|
||||
</View>
|
||||
</View>
|
||||
</Select.Root>
|
||||
</View>
|
||||
</SettingsList.Group>
|
||||
<SettingsList.Divider />
|
||||
|
||||
@@ -184,10 +184,14 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
||||
<Divider />
|
||||
|
||||
<View
|
||||
style={[a.w_full, a.py_lg, a.flex_row, a.gap_lg, a.align_center]}>
|
||||
style={[a.w_full, a.py_lg, a.flex_row, a.gap_md, a.align_center]}>
|
||||
<AppLanguageDropdown />
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, !gtMobile && a.text_md]}>
|
||||
style={[
|
||||
a.flex_1,
|
||||
t.atoms.text_contrast_medium,
|
||||
!gtMobile && a.text_md,
|
||||
]}>
|
||||
<Trans>Having trouble?</Trans>{' '}
|
||||
<InlineLinkText
|
||||
label={_(msg`Contact support`)}
|
||||
|
||||
@@ -325,3 +325,11 @@ input[type='range'][orient='vertical']::-moz-range-thumb {
|
||||
background-color: var(--backgroundLight);
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
/* #/components/Select/index.web.tsx */
|
||||
.radix-select-content {
|
||||
box-shadow: 0px 6px 24px -10px rgba(22, 23, 24, 0.25),
|
||||
0px 6px 12px -12px rgba(22, 23, 24, 0.15);
|
||||
min-width: var(--radix-select-trigger-width);
|
||||
max-height: var(--radix-select-content-available-height);
|
||||
}
|
||||
|
||||
@@ -78,7 +78,9 @@ export const SplashScreen = ({
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
]}>
|
||||
<AppLanguageDropdown />
|
||||
<View>
|
||||
<AppLanguageDropdown />
|
||||
</View>
|
||||
</View>
|
||||
<View style={{height: insets.bottom}} />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -154,9 +154,11 @@ function Footer() {
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
{top: 'auto'},
|
||||
a.p_xl,
|
||||
a.px_xl,
|
||||
a.py_lg,
|
||||
a.border_t,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.flex_wrap,
|
||||
a.gap_xl,
|
||||
a.flex_1,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {useCallback, useState} from 'react'
|
||||
import {Keyboard, StyleProp, View, ViewStyle} from 'react-native'
|
||||
import RNPickerSelect from 'react-native-picker-select'
|
||||
import {Keyboard, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -240,19 +239,21 @@ function SubtitleFileRow({
|
||||
numberOfLines={1}>
|
||||
{file.name}
|
||||
</Text>
|
||||
<RNPickerSelect
|
||||
placeholder={{
|
||||
label: _(msg`Select language...`),
|
||||
value: '',
|
||||
}}
|
||||
<select
|
||||
value={language}
|
||||
onValueChange={handleValueChange}
|
||||
items={otherLanguages.map(lang => ({
|
||||
label: `${lang.name} (${langCode(lang)})`,
|
||||
value: langCode(lang),
|
||||
}))}
|
||||
style={{viewContainer: {maxWidth: 200, flex: 1}}}
|
||||
/>
|
||||
onChange={evt => handleValueChange(evt.target.value)}
|
||||
style={{maxWidth: 200, flex: 1}}>
|
||||
<option value="" disabled selected hidden>
|
||||
{/* eslint-disable-next-line bsky-internal/avoid-unwrapped-text */}
|
||||
<Trans>Select language...</Trans>
|
||||
</option>
|
||||
{otherLanguages.map(lang => (
|
||||
<option key={langCode(lang)} value={langCode(lang)}>
|
||||
{/* eslint-disable-next-line bsky-internal/avoid-unwrapped-text */}
|
||||
{`${lang.name} (${langCode(lang)})`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import React, {memo} from 'react'
|
||||
import React, {memo, useCallback} from 'react'
|
||||
import {
|
||||
ActivityIndicator,
|
||||
AppState,
|
||||
Dimensions,
|
||||
LayoutAnimation,
|
||||
type ListRenderItemInfo,
|
||||
type StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type AppBskyActorDefs, AppBskyEmbedVideo} from '@atproto/api'
|
||||
import {
|
||||
type AppBskyActorDefs,
|
||||
AppBskyEmbedVideo,
|
||||
type AppBskyFeedDefs,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
@@ -51,6 +56,7 @@ import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||
import {FeedShutdownMsg} from './FeedShutdownMsg'
|
||||
import {PostFeedErrorMessage} from './PostFeedErrorMessage'
|
||||
import {PostFeedItem} from './PostFeedItem'
|
||||
import {ShowLessFollowup} from './ShowLessFollowup'
|
||||
import {ViewFullThread} from './ViewFullThread'
|
||||
|
||||
type FeedRow =
|
||||
@@ -117,6 +123,10 @@ type FeedRow =
|
||||
type: 'interstitialTrendingVideos'
|
||||
key: string
|
||||
}
|
||||
| {
|
||||
type: 'showLessFollowup'
|
||||
key: string
|
||||
}
|
||||
|
||||
export function getItemsForFeedback(feedRow: FeedRow):
|
||||
| {
|
||||
@@ -200,6 +210,20 @@ let PostFeed = ({
|
||||
const {rightNavVisible} = useLayoutBreakpoints()
|
||||
const areVideoFeedsEnabled = isNative
|
||||
|
||||
const [hasPressedShowLessUris, setHasPressedShowLessUris] = React.useState(
|
||||
() => new Set<string>(),
|
||||
)
|
||||
const onPressShowLess = useCallback(
|
||||
(interaction: AppBskyFeedDefs.Interaction) => {
|
||||
if (interaction.item) {
|
||||
const uri = interaction.item
|
||||
setHasPressedShowLessUris(prev => new Set([...prev, uri]))
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
const feedCacheKey = feedParams?.feedCacheKey
|
||||
const opts = React.useMemo(
|
||||
() => ({enabled, ignoreFilterFor}),
|
||||
@@ -321,6 +345,19 @@ let PostFeed = ({
|
||||
const {trendingDisabled, trendingVideoDisabled} = useTrendingSettings()
|
||||
|
||||
const feedItems: FeedRow[] = React.useMemo(() => {
|
||||
// wraps a slice item, and replaces it with a showLessFollowup item
|
||||
// if the user has pressed show less on it
|
||||
const sliceItem = (row: Extract<FeedRow, {type: 'sliceItem'}>) => {
|
||||
if (hasPressedShowLessUris.has(row.slice.items[row.indexInSlice]?.uri)) {
|
||||
return {
|
||||
type: 'showLessFollowup',
|
||||
key: row.key,
|
||||
} as const
|
||||
} else {
|
||||
return row
|
||||
}
|
||||
}
|
||||
|
||||
let feedKind: 'following' | 'discover' | 'profile' | 'thevids' | undefined
|
||||
if (feedType === 'following') {
|
||||
feedKind = 'following'
|
||||
@@ -450,43 +487,51 @@ let PostFeed = ({
|
||||
} else if (slice.isIncompleteThread && slice.items.length >= 3) {
|
||||
const beforeLast = slice.items.length - 2
|
||||
const last = slice.items.length - 1
|
||||
arr.push({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[0]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: 0,
|
||||
showReplyTo: false,
|
||||
})
|
||||
arr.push(
|
||||
sliceItem({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[0]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: 0,
|
||||
showReplyTo: false,
|
||||
}),
|
||||
)
|
||||
arr.push({
|
||||
type: 'sliceViewFullThread',
|
||||
key: slice._reactKey + '-viewFullThread',
|
||||
uri: slice.items[0].uri,
|
||||
})
|
||||
arr.push({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[beforeLast]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: beforeLast,
|
||||
showReplyTo:
|
||||
slice.items[beforeLast].parentAuthor?.did !==
|
||||
slice.items[beforeLast].post.author.did,
|
||||
})
|
||||
arr.push({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[last]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: last,
|
||||
showReplyTo: false,
|
||||
})
|
||||
arr.push(
|
||||
sliceItem({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[beforeLast]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: beforeLast,
|
||||
showReplyTo:
|
||||
slice.items[beforeLast].parentAuthor?.did !==
|
||||
slice.items[beforeLast].post.author.did,
|
||||
}),
|
||||
)
|
||||
arr.push(
|
||||
sliceItem({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[last]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: last,
|
||||
showReplyTo: false,
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
for (let i = 0; i < slice.items.length; i++) {
|
||||
arr.push({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[i]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: i,
|
||||
showReplyTo: i === 0,
|
||||
})
|
||||
arr.push(
|
||||
sliceItem({
|
||||
type: 'sliceItem',
|
||||
key: slice.items[i]._reactKey,
|
||||
slice: slice,
|
||||
indexInSlice: i,
|
||||
showReplyTo: i === 0,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -531,6 +576,7 @@ let PostFeed = ({
|
||||
gtMobile,
|
||||
isVideoFeed,
|
||||
areVideoFeedsEnabled,
|
||||
hasPressedShowLessUris,
|
||||
])
|
||||
|
||||
// events
|
||||
@@ -650,6 +696,7 @@ let PostFeed = ({
|
||||
isParentNotFound={item.isParentNotFound}
|
||||
hideTopBorder={rowIndex === 0 && indexInSlice === 0}
|
||||
rootPost={slice.items[0].post}
|
||||
onShowLess={onPressShowLess}
|
||||
/>
|
||||
)
|
||||
} else if (row.type === 'sliceViewFullThread') {
|
||||
@@ -684,6 +731,8 @@ let PostFeed = ({
|
||||
sourceContext={sourceContext}
|
||||
/>
|
||||
)
|
||||
} else if (row.type === 'showLessFollowup') {
|
||||
return <ShowLessFollowup />
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
@@ -700,6 +749,7 @@ let PostFeed = ({
|
||||
feedUriOrActorDid,
|
||||
feedTab,
|
||||
feedCacheKey,
|
||||
onPressShowLess,
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import {memo, useCallback, useMemo, useState} from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
type AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
ModerationDecision,
|
||||
type ModerationDecision,
|
||||
RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
type FontAwesomeIconStyle,
|
||||
} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {isReasonFeedSource, ReasonFeedSource} from '#/lib/api/feed/types'
|
||||
import {isReasonFeedSource, type ReasonFeedSource} from '#/lib/api/feed/types'
|
||||
import {MAX_POST_LINES} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
@@ -25,7 +25,11 @@ import {sanitizeDisplayName} from '#/lib/strings/display-names'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {countLines} from '#/lib/strings/helpers'
|
||||
import {s} from '#/lib/styles'
|
||||
import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
|
||||
import {
|
||||
POST_TOMBSTONE,
|
||||
type Shadow,
|
||||
usePostShadow,
|
||||
} from '#/state/cache/post-shadow'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {precacheProfile} from '#/state/queries/profile'
|
||||
import {useSession} from '#/state/session'
|
||||
@@ -43,7 +47,7 @@ import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/R
|
||||
import {ContentHider} from '#/components/moderation/ContentHider'
|
||||
import {LabelsOnMyPost} from '#/components/moderation/LabelsOnMe'
|
||||
import {PostAlerts} from '#/components/moderation/PostAlerts'
|
||||
import {AppModerationCause} from '#/components/Pills'
|
||||
import {type AppModerationCause} from '#/components/Pills'
|
||||
import {ProfileHoverCard} from '#/components/ProfileHoverCard'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {SubtleWebHover} from '#/components/SubtleWebHover'
|
||||
@@ -86,9 +90,11 @@ export function PostFeedItem({
|
||||
isParentBlocked,
|
||||
isParentNotFound,
|
||||
rootPost,
|
||||
onShowLess,
|
||||
}: FeedItemProps & {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
rootPost: AppBskyFeedDefs.PostView
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode {
|
||||
const postShadowed = usePostShadow(post)
|
||||
const richText = useMemo(
|
||||
@@ -122,6 +128,7 @@ export function PostFeedItem({
|
||||
isParentBlocked={isParentBlocked}
|
||||
isParentNotFound={isParentNotFound}
|
||||
rootPost={rootPost}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -144,23 +151,27 @@ let FeedItemInner = ({
|
||||
isParentBlocked,
|
||||
isParentNotFound,
|
||||
rootPost,
|
||||
onShowLess,
|
||||
}: FeedItemProps & {
|
||||
richText: RichTextAPI
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
rootPost: AppBskyFeedDefs.PostView
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const queryClient = useQueryClient()
|
||||
const {openComposer} = useComposerControls()
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
|
||||
const [hover, setHover] = useState(false)
|
||||
|
||||
const href = useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey)
|
||||
}, [post.uri, post.author])
|
||||
const {sendInteraction} = useFeedFeedbackContext()
|
||||
|
||||
const onPressReply = React.useCallback(() => {
|
||||
const onPressReply = useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#interactionReply',
|
||||
@@ -178,7 +189,7 @@ let FeedItemInner = ({
|
||||
})
|
||||
}, [post, record, openComposer, moderation, sendInteraction, feedContext])
|
||||
|
||||
const onOpenAuthor = React.useCallback(() => {
|
||||
const onOpenAuthor = useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughAuthor',
|
||||
@@ -186,7 +197,7 @@ let FeedItemInner = ({
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenReposter = React.useCallback(() => {
|
||||
const onOpenReposter = useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughReposter',
|
||||
@@ -194,7 +205,7 @@ let FeedItemInner = ({
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onOpenEmbed = React.useCallback(() => {
|
||||
const onOpenEmbed = useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughEmbed',
|
||||
@@ -202,7 +213,7 @@ let FeedItemInner = ({
|
||||
})
|
||||
}, [sendInteraction, post, feedContext])
|
||||
|
||||
const onBeforePress = React.useCallback(() => {
|
||||
const onBeforePress = useCallback(() => {
|
||||
sendInteraction({
|
||||
item: post.uri,
|
||||
event: 'app.bsky.feed.defs#clickthroughItem',
|
||||
@@ -240,7 +251,6 @@ let FeedItemInner = ({
|
||||
? rootPost.threadgate.record
|
||||
: undefined
|
||||
|
||||
const [hover, setHover] = useState(false)
|
||||
return (
|
||||
<Link
|
||||
testID={`feedItem-by-${post.author.handle}`}
|
||||
@@ -427,6 +437,7 @@ let FeedItemInner = ({
|
||||
logContext="FeedItem"
|
||||
feedContext={feedContext}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -461,7 +472,7 @@ let PostContent = ({
|
||||
const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
|
||||
threadgateRecord,
|
||||
})
|
||||
const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
|
||||
const additionalPostAlerts: AppModerationCause[] = useMemo(() => {
|
||||
const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
|
||||
const rootPostUri = bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
post.record,
|
||||
@@ -482,7 +493,7 @@ let PostContent = ({
|
||||
: []
|
||||
}, [post, currentAccount?.did, threadgateHiddenReplies])
|
||||
|
||||
const onPressShowMore = React.useCallback(() => {
|
||||
const onPressShowMore = useCallback(() => {
|
||||
setLimitLines(false)
|
||||
}, [setLimitLines])
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import {View} from 'react-native'
|
||||
import {Trans} from '@lingui/macro'
|
||||
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {CircleCheck_Stroke2_Corner0_Rounded} from '#/components/icons/CircleCheck'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export function ShowLessFollowup() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
t.atoms.border_contrast_low,
|
||||
a.border_t,
|
||||
t.atoms.bg_contrast_25,
|
||||
a.p_sm,
|
||||
]}>
|
||||
<View
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
t.atoms.border_contrast_low,
|
||||
a.border,
|
||||
a.rounded_sm,
|
||||
a.p_md,
|
||||
a.flex_row,
|
||||
a.gap_sm,
|
||||
]}>
|
||||
<CircleCheck_Stroke2_Corner0_Rounded
|
||||
style={[t.atoms.text_contrast_low]}
|
||||
size="sm"
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
t.atoms.text_contrast_medium,
|
||||
a.leading_snug,
|
||||
]}>
|
||||
<Trans>
|
||||
Thank you for your feedback! It has been sent to the feed operator.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {memo, useMemo, useState} from 'react'
|
||||
import {memo, useMemo, useState} from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
@@ -6,16 +6,17 @@ import {
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
RichText as RichTextAPI,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import type React from 'react'
|
||||
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {Shadow} from '#/state/cache/post-shadow'
|
||||
import {type Shadow} from '#/state/cache/post-shadow'
|
||||
import {atoms as a, useTheme as useAlf} from '#/alf'
|
||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||
import {useMenuControl} from '#/components/Menu'
|
||||
@@ -34,6 +35,7 @@ let PostDropdownBtn = ({
|
||||
size,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
@@ -45,6 +47,7 @@ let PostDropdownBtn = ({
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const theme = useTheme()
|
||||
const alf = useAlf()
|
||||
@@ -100,6 +103,7 @@ let PostDropdownBtn = ({
|
||||
richText={richText}
|
||||
timestamp={timestamp}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
)}
|
||||
</Menu.Root>
|
||||
|
||||
@@ -17,6 +17,8 @@ import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
||||
import {IS_INTERNAL} from '#/lib/app-info'
|
||||
import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
|
||||
import {useOpenLink} from '#/lib/hooks/useOpenLink'
|
||||
import {getCurrentRoute} from '#/lib/routes/helpers'
|
||||
import {makeProfileLink} from '#/lib/routes/links'
|
||||
@@ -60,6 +62,7 @@ import {
|
||||
} from '#/components/dialogs/PostInteractionSettingsDialog'
|
||||
import {SendViaChatDialog} from '#/components/dms/dialogs/ShareViaChatDialog'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {Atom_Stroke2_Corner0_Rounded as AtomIcon} from '#/components/icons/Atom'
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
@@ -98,6 +101,7 @@ let PostDropdownMenuItems = ({
|
||||
richText,
|
||||
timestamp,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
testID: string
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
@@ -109,6 +113,7 @@ let PostDropdownMenuItems = ({
|
||||
size?: 'lg' | 'md' | 'sm'
|
||||
timestamp: string
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
@@ -300,8 +305,15 @@ let PostDropdownMenuItems = ({
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}, [feedFeedback, postUri, postFeedContext, _])
|
||||
if (onShowLess) {
|
||||
onShowLess({
|
||||
item: postUri,
|
||||
feedContext: postFeedContext,
|
||||
})
|
||||
} else {
|
||||
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
|
||||
}
|
||||
}, [feedFeedback, postUri, postFeedContext, _, onShowLess])
|
||||
|
||||
const onSelectChatToShareTo = React.useCallback(
|
||||
(conversation: string) => {
|
||||
@@ -430,6 +442,13 @@ let PostDropdownMenuItems = ({
|
||||
shareText(postAuthor.did)
|
||||
}, [postAuthor.did])
|
||||
|
||||
const onReportMisclassification = useCallback(() => {
|
||||
const url = `https://docs.google.com/forms/d/e/1FAIpQLSd0QPqhNFksDQf1YyOos7r1ofCLvmrKAH1lU042TaS3GAZaWQ/viewform?entry.1756031717=${toShareUrl(
|
||||
href,
|
||||
)}`
|
||||
openLink(url)
|
||||
}, [href, openLink])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Outer>
|
||||
@@ -539,6 +558,19 @@ let PostDropdownMenuItems = ({
|
||||
<Menu.ItemText>{_(msg`Show less like this`)}</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={EmojiSad} position="right" />
|
||||
</Menu.Item>
|
||||
|
||||
{IS_INTERNAL &&
|
||||
DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && (
|
||||
<Menu.Item
|
||||
testID="postDropdownReportMisclassificationBtn"
|
||||
label={_(msg`Report topic misclassification`)}
|
||||
onPress={onReportMisclassification}>
|
||||
<Menu.ItemText>
|
||||
{_(msg`Report topic misclassification`)}
|
||||
</Menu.ItemText>
|
||||
<Menu.ItemIcon icon={AtomIcon} position="right" />
|
||||
</Menu.Item>
|
||||
)}
|
||||
</Menu.Group>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -8,11 +8,11 @@ import {
|
||||
} from 'react-native'
|
||||
import * as Clipboard from 'expo-clipboard'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyFeedThreadgate,
|
||||
type AppBskyFeedDefs,
|
||||
type AppBskyFeedPost,
|
||||
type AppBskyFeedThreadgate,
|
||||
AtUri,
|
||||
RichText as RichTextAPI,
|
||||
type RichText as RichTextAPI,
|
||||
} from '@atproto/api'
|
||||
import {msg, plural} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -26,7 +26,7 @@ import {makeProfileLink} from '#/lib/routes/links'
|
||||
import {shareUrl} from '#/lib/sharing'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {Shadow} from '#/state/cache/types'
|
||||
import {type Shadow} from '#/state/cache/types'
|
||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||
import {
|
||||
usePostLikeMutationQueue,
|
||||
@@ -60,6 +60,7 @@ let PostCtrls = ({
|
||||
onPostReply,
|
||||
logContext,
|
||||
threadgateRecord,
|
||||
onShowLess,
|
||||
}: {
|
||||
big?: boolean
|
||||
post: Shadow<AppBskyFeedDefs.PostView>
|
||||
@@ -71,6 +72,7 @@ let PostCtrls = ({
|
||||
onPostReply?: (postUri: string | undefined) => void
|
||||
logContext: 'FeedItem' | 'PostThreadItem' | 'Post' | 'ImmersiveVideo'
|
||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||
onShowLess?: (interaction: AppBskyFeedDefs.Interaction) => void
|
||||
}): React.ReactNode => {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
@@ -378,6 +380,7 @@ let PostCtrls = ({
|
||||
hitSlop={POST_CTRL_HITSLOP}
|
||||
timestamp={post.indexedAt}
|
||||
threadgateRecord={threadgateRecord}
|
||||
onShowLess={onShowLess}
|
||||
/>
|
||||
</View>
|
||||
{isDiscoverDebugUser && feedContext && (
|
||||
|
||||
@@ -65,7 +65,7 @@ let NavSignupCard = ({}: {}): React.ReactNode => {
|
||||
</View>
|
||||
|
||||
<View style={[a.mt_md, a.w_full, {height: 32}]}>
|
||||
<AppLanguageDropdown style={{marginTop: 0}} />
|
||||
<AppLanguageDropdown />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
@@ -137,7 +137,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
|
||||
{!hasSession && leftNavMinimal && (
|
||||
<View style={[a.w_full, {height: 32}]}>
|
||||
<AppLanguageDropdown style={{marginTop: 0}} />
|
||||
<AppLanguageDropdown />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -14290,11 +14290,6 @@ lodash.isequal@^4.5.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
|
||||
|
||||
lodash.isobject@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isobject/-/lodash.isobject-3.0.2.tgz#3c8fb8d5b5bf4bf90ae06e14f2a530a4ed935e1d"
|
||||
integrity sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
@@ -16840,14 +16835,6 @@ react-native-pager-view@6.7.1:
|
||||
resolved "https://registry.yarnpkg.com/react-native-pager-view/-/react-native-pager-view-6.7.1.tgz#60d52dedbcc92ee7037a13287ebeed5f74e49df7"
|
||||
integrity sha512-cBSr6xw4g5N7Kd3VGWcf+kmaH7iBWb0DXAf2bVo3bXkzBcBbTOmYSvc0LVLHhUPW8nEq5WjT9LCIYAzgF++EXw==
|
||||
|
||||
react-native-picker-select@^9.3.1:
|
||||
version "9.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-native-picker-select/-/react-native-picker-select-9.3.1.tgz#8a2ad51c286fcd54ef60fb883842ec1895c15003"
|
||||
integrity sha512-o621HcsKJfJkpYeP/PZQiZTKbf8W7FT08niLFL0v1pGkIQyak5IfzfinV2t+/l1vktGwAH2Tt29LrP/Hc5fk3A==
|
||||
dependencies:
|
||||
lodash.isequal "^4.5.0"
|
||||
lodash.isobject "^3.0.2"
|
||||
|
||||
react-native-progress@bluesky-social/react-native-progress:
|
||||
version "5.0.0"
|
||||
resolved "https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
|
||||
|
||||
Reference in New Issue
Block a user