This commit is contained in:
Hailey
2025-05-06 10:56:06 -07:00
26 changed files with 1179 additions and 451 deletions
@@ -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
}
@@ -161,48 +161,31 @@ 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() {
guard let scrollViewTag = scrollViewTag else {
return
}
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: couldnt find UIScrollView under tag \(tag)")
return
}
self.scrollView = sv
self.rctRefreshCtrl = sv.refreshControl as? RCTRefreshControl
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() {
-1
View File
@@ -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",
+3
View File
@@ -204,6 +204,9 @@ export const atoms = {
flex_grow: {
flexGrow: 1,
},
flex_grow_0: {
flexGrow: 0,
},
flex_shrink: {
flexShrink: 1,
},
+47 -40
View File
@@ -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>
)
}
+11 -2
View File
@@ -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,
+289
View File
@@ -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>
}
+280
View File
@@ -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>
)
}
+185
View File
@@ -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>
}
+59 -59
View File
@@ -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 ""
+42 -139
View File
@@ -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={{}}
<Select.Root
value={sanitizeAppLanguageSetting(langPrefs.appLanguage)}
onValueChange={onChangeAppLanguage}
items={APP_LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
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={{}}
<Select.Root
value={langPrefs.primaryLanguage}
onValueChange={onChangePrimaryLanguage}
items={LANGUAGES.filter(l => Boolean(l.code2)).map(l => ({
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 />
+6 -2
View File
@@ -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`)}
+8
View File
@@ -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);
}
+2
View File
@@ -78,8 +78,10 @@ export const SplashScreen = ({
a.justify_center,
a.align_center,
]}>
<View>
<AppLanguageDropdown />
</View>
</View>
<View style={{height: insets.bottom}} />
</ErrorBoundary>
</CenteredView>
+3 -1
View File
@@ -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,
+15 -14
View File
@@ -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>
+60 -10
View File
@@ -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,19 +487,22 @@ let PostFeed = ({
} else if (slice.isIncompleteThread && slice.items.length >= 3) {
const beforeLast = slice.items.length - 2
const last = slice.items.length - 1
arr.push({
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({
arr.push(
sliceItem({
type: 'sliceItem',
key: slice.items[beforeLast]._reactKey,
slice: slice,
@@ -470,23 +510,28 @@ let PostFeed = ({
showReplyTo:
slice.items[beforeLast].parentAuthor?.did !==
slice.items[beforeLast].post.author.did,
})
arr.push({
}),
)
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({
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,
],
)
+26 -15
View File
@@ -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])
+46
View File
@@ -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>
)
}
+10 -6
View File
@@ -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,
})
if (onShowLess) {
onShowLess({
item: postUri,
feedContext: postFeedContext,
})
} else {
Toast.show(_(msg({message: 'Feedback sent!', context: 'toast'})))
}, [feedFeedback, postUri, postFeedContext, _])
}
}, [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 -5
View File
@@ -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 && (
+1 -1
View File
@@ -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>
)
+1 -1
View File
@@ -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>
-13
View File
@@ -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"