From 04dc6dc9ca3cdc747004367982313fd8bc157507 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 6 May 2025 17:03:48 +0300 Subject: [PATCH 1/7] add internal topic misclassification report button (#8332) --- .../util/forms/PostDropdownBtnMenuItems.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx index 6be813e365..e50a2d3e43 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' @@ -430,6 +433,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 +549,19 @@ let PostDropdownMenuItems = ({ {_(msg`Show less like this`)} + + {IS_INTERNAL && + DISCOVER_DEBUG_DIDS[currentAccount?.did ?? ''] && ( + + + {_(msg`Report topic misclassification`)} + + + + )} )} From 25f8506c4152840e83ba9210452b60ea5cc0987f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 6 May 2025 17:34:50 +0300 Subject: [PATCH 2/7] Remove post from feed after pressing show less (#8333) * remove post from feed after pressing show less * fix text overflow on android * move state up so it won't get recycled away * make type optional --- src/view/com/posts/PostFeed.tsx | 114 +++++++++++++----- src/view/com/posts/PostFeedItem.tsx | 41 ++++--- src/view/com/posts/ShowLessFollowup.tsx | 46 +++++++ src/view/com/util/forms/PostDropdownBtn.tsx | 16 ++- .../util/forms/PostDropdownBtnMenuItems.tsx | 13 +- src/view/com/util/post-ctrls/PostCtrls.tsx | 13 +- 6 files changed, 183 insertions(+), 60 deletions(-) create mode 100644 src/view/com/posts/ShowLessFollowup.tsx 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 e50a2d3e43..9c3d709d93 100644 --- a/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx +++ b/src/view/com/util/forms/PostDropdownBtnMenuItems.tsx @@ -101,6 +101,7 @@ let PostDropdownMenuItems = ({ richText, timestamp, threadgateRecord, + onShowLess, }: { testID: string post: Shadow @@ -112,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() @@ -303,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) => { 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 && ( From 973538d246a3f76550611e438152f1a6cad75f49 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 6 May 2025 20:27:05 +0300 Subject: [PATCH 3/7] New `Select` component (#8323) * radix select component on web * native implementation (wip) * fix sheet height/padding * tone down web styles * react 19 cleanup * replace primary language select * change style on native * get auto placeholder working * more style tweaks * replace app language dropdown * replace rnpickerselect with native select * rm react-native-picker-select dependency * rm placeholder, since a value is always selected * docblock for renderItem * add more docblocks * add style prop to item * pass selectedValue through renderItem * fix context * Style overflow buttons --------- Co-authored-by: Eric Bailey --- .../src/BottomSheetNativeComponent.tsx | 5 +- package.json | 1 - src/alf/atoms.ts | 3 + src/components/AppLanguageDropdown.tsx | 87 +++--- src/components/AppLanguageDropdown.web.tsx | 83 ----- src/components/Dialog/shared.tsx | 11 +- src/components/Select/index.tsx | 289 ++++++++++++++++++ src/components/Select/index.web.tsx | 280 +++++++++++++++++ src/components/Select/types.ts | 185 +++++++++++ src/screens/Settings/LanguageSettings.tsx | 185 +++-------- src/screens/Signup/index.tsx | 8 +- src/style.css | 8 + src/view/com/auth/SplashScreen.tsx | 4 +- src/view/com/auth/SplashScreen.web.tsx | 4 +- .../com/composer/videos/SubtitleDialog.tsx | 29 +- src/view/shell/NavSignupCard.tsx | 2 +- src/view/shell/desktop/RightNav.tsx | 2 +- yarn.lock | 13 - 18 files changed, 899 insertions(+), 300 deletions(-) delete mode 100644 src/components/AppLanguageDropdown.web.tsx create mode 100644 src/components/Select/index.tsx create mode 100644 src/components/Select/index.web.tsx create mode 100644 src/components/Select/types.ts 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/package.json b/package.json index 0d96e688cb..5d308958ea 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..eec47b2ba4 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 { + LayoutChangeEvent, + StyleProp, + TextStyle, + View, + ViewStyle, +} from 'react-native' 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/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/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 747087e83a..cc321e65f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14285,11 +14285,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" From 521ec8e044e58633530e1864e7abc6e22554d7d3 Mon Sep 17 00:00:00 2001 From: hailey Date: Tue, 6 May 2025 10:54:08 -0700 Subject: [PATCH 4/7] swap out cropper library (#8327) * mostly implement * type errors * unused import * rm comment * stop accidentally deleting the image while compressing * upgrade * type fixes * upgrade, remove timeout * bump * rm mock * bump --------- Co-authored-by: Samuel Newman --- __mocks__/react-native-image-crop-picker.js | 9 ---- package.json | 2 +- ...eact-native-image-crop-picker+0.42.0.patch | 48 ------------------- src/lib/media/manip.ts | 25 ++++++---- src/lib/media/manip.web.ts | 14 +++--- src/lib/media/picker.e2e.tsx | 17 +++---- src/lib/media/picker.shared.ts | 11 ++++- src/lib/media/picker.tsx | 48 +++++++++---------- src/lib/media/picker.web.tsx | 26 +++++----- src/lib/media/types.ts | 7 --- src/screens/Onboarding/StepProfile/index.tsx | 8 ++-- .../Profile/Header/EditProfileDialog.tsx | 10 ++-- src/state/gallery.ts | 16 +------ src/state/modals/index.tsx | 4 +- src/state/queries/list.ts | 22 ++++----- src/state/queries/profile.ts | 6 +-- .../com/composer/photos/OpenCameraBtn.tsx | 4 +- src/view/com/modals/CreateOrEditList.tsx | 6 +-- src/view/com/modals/CropImage.web.tsx | 6 +-- src/view/com/modals/EditProfile.tsx | 10 ++-- src/view/com/util/UserAvatar.tsx | 31 ++++++------ src/view/com/util/UserBanner.tsx | 20 ++++---- yarn.lock | 10 ++-- 23 files changed, 145 insertions(+), 215 deletions(-) delete mode 100644 __mocks__/react-native-image-crop-picker.js delete mode 100644 patches/react-native-image-crop-picker+0.42.0.patch diff --git a/__mocks__/react-native-image-crop-picker.js b/__mocks__/react-native-image-crop-picker.js deleted file mode 100644 index 9f901dfacb..0000000000 --- a/__mocks__/react-native-image-crop-picker.js +++ /dev/null @@ -1,9 +0,0 @@ -export const openPicker = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) -export const openCamera = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) -export const openCropper = jest - .fn() - .mockImplementation(() => Promise.resolve({uri: ''})) diff --git a/package.json b/package.json index 5d308958ea..5a3a801862 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "expo-font": "~13.3.0", "expo-haptics": "~14.1.4", "expo-image": "~2.1.6", + "expo-image-crop-tool": "^0.1.7", "expo-image-manipulator": "~13.1.5", "expo-image-picker": "~16.1.4", "expo-linear-gradient": "~14.1.4", @@ -188,7 +189,6 @@ "react-native-edge-to-edge": "^1.6.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "~1.11.0", - "react-native-image-crop-picker": "^0.42.0", "react-native-ios-context-menu": "^1.15.3", "react-native-keyboard-controller": "^1.17.1", "react-native-mmkv": "^2.12.2", diff --git a/patches/react-native-image-crop-picker+0.42.0.patch b/patches/react-native-image-crop-picker+0.42.0.patch deleted file mode 100644 index c6124ad1e1..0000000000 --- a/patches/react-native-image-crop-picker+0.42.0.patch +++ /dev/null @@ -1,48 +0,0 @@ -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -index a08629b..fab6299 100644 ---- a/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -+++ b/node_modules/react-native-image-crop-picker/android/src/main/AndroidManifest.xml -@@ -24,7 +24,7 @@ - - -+ android:theme="@style/Theme.UCropNoEdgeToEdge" /> - - - -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml -new file mode 100644 -index 0000000..5301f74 ---- /dev/null -+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values-v35/styles.xml -@@ -0,0 +1,5 @@ -+ -+ -+ -\ No newline at end of file -diff --git a/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml -new file mode 100644 -index 0000000..55569aa ---- /dev/null -+++ b/node_modules/react-native-image-crop-picker/android/src/main/res/values/styles.xml -@@ -0,0 +1,3 @@ -+ -+