diff --git a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx index 869b89e3d4..d367ac300c 100644 --- a/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx +++ b/modules/bottom-sheet/src/BottomSheetNativeComponent.tsx @@ -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, diff --git a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift index e6ebef1d33..15993ef29e 100644 --- a/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift +++ b/modules/expo-scroll-forwarder/ios/ExpoScrollForwarderView.swift @@ -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() { diff --git a/package.json b/package.json index df0f83921f..2fab82975a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/alf/atoms.ts b/src/alf/atoms.ts index c60af48625..68aa3cc882 100644 --- a/src/alf/atoms.ts +++ b/src/alf/atoms.ts @@ -204,6 +204,9 @@ export const atoms = { flex_grow: { flexGrow: 1, }, + flex_grow_0: { + flexGrow: 0, + }, flex_shrink: { flexShrink: 1, }, diff --git a/src/components/AppLanguageDropdown.tsx b/src/components/AppLanguageDropdown.tsx index de2e50fc8c..9837ce5ced 100644 --- a/src/components/AppLanguageDropdown.tsx +++ b/src/components/AppLanguageDropdown.tsx @@ -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[0]) => { + (value: string) => { if (!value) return if (sanitizedLang !== value) { setLangPrefs.setAppLanguage(sanitizeAppLanguageSetting(value)) @@ -32,43 +34,48 @@ export function AppLanguageDropdown(_props: ViewStyleProp) { ) return ( - - Boolean(l.code2)).map(l => ({ + + + {({props}) => ( + + )} + + ( + + + {label} + + )} + 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, - }, - }} /> - - - - - + ) } diff --git a/src/components/AppLanguageDropdown.web.tsx b/src/components/AppLanguageDropdown.web.tsx deleted file mode 100644 index d51b53ac07..0000000000 --- a/src/components/AppLanguageDropdown.web.tsx +++ /dev/null @@ -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) => { - 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 ( - - - - {APP_LANGUAGES.find(l => l.code2 === sanitizedLang)?.name} - - - - - - - ) -} diff --git a/src/components/Dialog/shared.tsx b/src/components/Dialog/shared.tsx index 44a4f6b0bb..40d0408788 100644 --- a/src/components/Dialog/shared.tsx +++ b/src/components/Dialog/shared.tsx @@ -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 + onLayout?: (event: LayoutChangeEvent) => void }) { const t = useTheme() return ( + +const Context = createContext(null) + +const ValueTextContext = createContext< + [any, React.Dispatch>] +>([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() + + const ctx = useMemo( + () => ({ + control, + value, + onValueChange, + disabled, + }), + [control, value, onValueChange, disabled], + ) + return ( + + + {children} + + + ) +} + +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 ( + + ) + } +} + +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 ( + {text} + ) +} + +export function Icon({}: IconProps) { + return +} + +export function Content({ + items, + valueExtractor = defaultItemValueExtractor, + ...props +}: ContentProps) { + 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 ( + + + + ) +} + +function ContentInner({ + items, + renderItem, + valueExtractor, + ...context +}: ContentProps & 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( + () => ( + + ), + [control, _], + ) + + return ( + + setHeaderHeight(evt.nativeEvent.layout.height)} + style={[a.absolute, a.top_0, a.left_0, a.right_0, a.z_10]}> + + Select an option + + + + + ) +} + +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 ( + + ) +} + +export function ItemText({children}: ItemTextProps) { + const {selected} = useItemContext() + const t = useTheme() + + // eslint-disable-next-line bsky-internal/avoid-unwrapped-text + return ( + + {children} + + ) +} + +export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { + const {selected} = useItemContext() + + return {selected && } +} diff --git a/src/components/Select/index.web.tsx b/src/components/Select/index.web.tsx new file mode 100644 index 0000000000..e9d26631c5 --- /dev/null +++ b/src/components/Select/index.web.tsx @@ -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(null) + +export function Root(props: RootProps) { + return ( + + + + ) +} + +const RadixTriggerPassThrough = forwardRef( + ( + props: { + children: ( + props: RadixPassThroughTriggerProps & { + ref: React.Ref + }, + ) => 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 ( + + + {props => + children({ + isNative: false, + state: { + hovered, + focused, + pressed: false, + }, + props: { + ...props, + onFocus: onFocus, + onBlur: onBlur, + onMouseEnter, + onMouseLeave, + accessibilityLabel: label, + }, + }) + } + + + ) + } else { + return ( + + {children} + + ) + } +} + +export function ValueText({children: _, style, ...props}: ValueProps) { + return ( + + + + ) +} + +export function Icon({style}: IconProps) { + const t = useTheme() + return ( + + + + ) +} + +export function Content({items, renderItem}: ContentProps) { + 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 ( + + + + + + + + {items.map((item, index) => renderItem(item, index, selectedValue))} + + + + + + + + ) +} + +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 ( + + {children} + + ) +} + +export const ItemText = RadixSelect.ItemText + +export function ItemIndicator({icon: Icon = CheckIcon}: ItemIndicatorProps) { + return ( + + + + ) +} diff --git a/src/components/Select/types.ts b/src/components/Select/types.ts new file mode 100644 index 0000000000..5c1b80a3be --- /dev/null +++ b/src/components/Select/types.ts @@ -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 +} + +/* + * 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 = { + /** + * 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}) => ( + * + * + * {label} + * + * )} + * ``` + */ + 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 + value: string + label: string + children: React.ReactNode + style?: StyleProp +} + +export type ItemTextProps = { + children: React.ReactNode +} + +export type ItemIndicatorProps = { + icon?: React.ComponentType +} diff --git a/src/locale/locales/en/messages.po b/src/locale/locales/en/messages.po index 6ad7d50f30..122e9afe4f 100644 --- a/src/locale/locales/en/messages.po +++ b/src/locale/locales/en/messages.po @@ -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." 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 "" diff --git a/src/screens/Settings/LanguageSettings.tsx b/src/screens/Settings/LanguageSettings.tsx index 7266dda4ae..d035e65926 100644 --- a/src/screens/Settings/LanguageSettings.tsx +++ b/src/screens/Settings/LanguageSettings.tsx @@ -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 export function LanguageSettingsScreen({}: Props) { const {_} = useLingui() @@ -32,7 +39,7 @@ export function LanguageSettingsScreen({}: Props) { }, [openModal]) const onChangePrimaryLanguage = useCallback( - (value: Parameters[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[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. - - Boolean(l.code2)).map(l => ({ + + + + + + ( + + + {label} + + )} + 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, - }, - }} /> - - - - - + @@ -171,77 +125,26 @@ export function LanguageSettingsScreen({}: Props) { Select your preferred language for translations in your feed. - - Boolean(l.code2)).map(l => ({ + + + + + + ( + + + {label} + + )} + 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, - }, - }} /> - - - - - + diff --git a/src/screens/Signup/index.tsx b/src/screens/Signup/index.tsx index c980400104..03f4e2cdd2 100644 --- a/src/screens/Signup/index.tsx +++ b/src/screens/Signup/index.tsx @@ -184,10 +184,14 @@ export function Signup({onPressBack}: {onPressBack: () => void}) { + style={[a.w_full, a.py_lg, a.flex_row, a.gap_md, a.align_center]}> + style={[ + a.flex_1, + t.atoms.text_contrast_medium, + !gtMobile && a.text_md, + ]}> Having trouble?{' '} - + + + diff --git a/src/view/com/auth/SplashScreen.web.tsx b/src/view/com/auth/SplashScreen.web.tsx index 30ebb391fc..f3488e485d 100644 --- a/src/view/com/auth/SplashScreen.web.tsx +++ b/src/view/com/auth/SplashScreen.web.tsx @@ -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, diff --git a/src/view/com/composer/videos/SubtitleDialog.tsx b/src/view/com/composer/videos/SubtitleDialog.tsx index 13d1b7ce55..298e70896a 100644 --- a/src/view/com/composer/videos/SubtitleDialog.tsx +++ b/src/view/com/composer/videos/SubtitleDialog.tsx @@ -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} - ({ - 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}}> + + {otherLanguages.map(lang => ( + + ))} + diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 3a6b8f6601..181b35026d 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -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(), + ) + 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) => { + 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 } else { return null } @@ -700,6 +749,7 @@ let PostFeed = ({ feedUriOrActorDid, feedTab, feedCacheKey, + onPressShowLess, ], ) diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 499b9ccd56..facd31e5fc 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -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 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 ( @@ -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( post.record, @@ -482,7 +493,7 @@ let PostContent = ({ : [] }, [post, currentAccount?.did, threadgateHiddenReplies]) - const onPressShowMore = React.useCallback(() => { + const onPressShowMore = useCallback(() => { setLimitLines(false) }, [setLimitLines]) diff --git a/src/view/com/posts/ShowLessFollowup.tsx b/src/view/com/posts/ShowLessFollowup.tsx new file mode 100644 index 0000000000..01412b9a13 --- /dev/null +++ b/src/view/com/posts/ShowLessFollowup.tsx @@ -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 ( + + + + + + Thank you for your feedback! It has been sent to the feed operator. + + + + + ) +} diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index fd577605a9..c50b36640f 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -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 @@ -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} /> )} diff --git a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx index 6be813e365..9c3d709d93 100644 --- a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx +++ b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx @@ -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 @@ -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 ( <> @@ -539,6 +558,19 @@ let PostDropdownMenuItems = ({ {_(msg`Show less like this`)} + + {IS_INTERNAL && + DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( + + + {_(msg`Report topic misclassification`)} + + + + )} )} diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index fe583e8017..d97654a633 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -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 @@ -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} /> {isDiscoverDebugUser && feedContext && ( diff --git a/src/view/shell/NavSignupCard.tsx b/src/view/shell/NavSignupCard.tsx index e32c24dc43..000f5824c0 100644 --- a/src/view/shell/NavSignupCard.tsx +++ b/src/view/shell/NavSignupCard.tsx @@ -65,7 +65,7 @@ let NavSignupCard = ({}: {}): React.ReactNode => { - + ) diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx index 18ce42ee8b..26795e0fd9 100644 --- a/src/view/shell/desktop/RightNav.tsx +++ b/src/view/shell/desktop/RightNav.tsx @@ -137,7 +137,7 @@ export function DesktopRightNav({routeName}: {routeName: string}) { {!hasSession && leftNavMinimal && ( - + )} diff --git a/yarn.lock b/yarn.lock index eeb876eb69..20458e9065 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"