diff --git a/app.config.js b/app.config.js index fc70ca3332..57de4493de 100644 --- a/app.config.js +++ b/app.config.js @@ -99,7 +99,7 @@ module.exports = function (config) { 'tr', 'uk', 'vi', - 'yue-Hant', + 'yue', 'zh-Hans', 'zh-Hant', ], @@ -226,8 +226,12 @@ module.exports = function (config) { }, ], 'react-native-compressor', - // TODO: Reenable when the build issue is fixed. - // '@bitdrift/react-native', + [ + '@bitdrift/react-native', + { + networkInstrumentation: true, + }, + ], './plugins/starterPackAppClipExtension/withStarterPackAppClip.js', './plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js', diff --git a/package.json b/package.json index 991a08427f..88ef900238 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.96.7", + "version": "1.97.0", "private": true, "engines": { "node": ">=20" @@ -55,6 +55,7 @@ }, "dependencies": { "@atproto/api": "^0.13.28", + "@bitdrift/react-native": "^0.6.2", "@braintree/sanitize-url": "^6.0.2", "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", "@emoji-mart/react": "^1.1.1", diff --git a/src/components/Layout/index.tsx b/src/components/Layout/index.tsx index 8532cbbb49..489ebb2257 100644 --- a/src/components/Layout/index.tsx +++ b/src/components/Layout/index.tsx @@ -58,6 +58,7 @@ export const Content = React.memo(function Content({ contentContainerStyle, ...props }: ContentProps) { + const t = useTheme() const {footerHeight} = useShellLayout() const animatedProps = useAnimatedProps(() => { return { @@ -73,6 +74,7 @@ export const Content = React.memo(function Content({ <Animated.ScrollView id="content" automaticallyAdjustsScrollIndicatorInsets={false} + indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} // sets the scroll inset to the height of the footer animatedProps={animatedProps} style={[scrollViewStyles.common, style]} diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 3cd593a106..50e741ea7b 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -15,7 +15,6 @@ import { linkRequiresWarning, } from '#/lib/strings/url-helpers' import {isNative, isWeb} from '#/platform/detection' -import {shouldClickOpenNewTab} from '#/platform/urls' import {useModalControls} from '#/state/modals' import {atoms as a, flatten, TextStyleProp, useTheme, web} from '#/alf' import {Button, ButtonProps} from '#/components/Button' @@ -55,6 +54,12 @@ type BaseLinkProps = Pick< */ onPress?: (e: GestureResponderEvent) => void | false + /** + * Callback for when the link is long pressed (on native). Prevent default + * and return `false` to exit early and prevent default long press hander. + */ + onLongPress?: (e: GestureResponderEvent) => void | false + /** * Web-only attribute. Sets `download` attr on web. */ @@ -72,6 +77,7 @@ export function useLink({ action = 'push', disableMismatchWarning, onPress: outerOnPress, + onLongPress: outerOnLongPress, shareOnLongPress, }: BaseLinkProps & { displayText: string @@ -175,8 +181,14 @@ export function useLink({ } }, [disableMismatchWarning, displayText, href, isExternal, openModal]) - const onLongPress = - isNative && isExternal && shareOnLongPress ? handleLongPress : undefined + const onLongPress = React.useCallback( + (e: GestureResponderEvent) => { + const exitEarlyIfFalse = outerOnLongPress?.(e) + if (exitEarlyIfFalse === false) return + return isNative && shareOnLongPress ? handleLongPress() : undefined + }, + [outerOnLongPress, handleLongPress, shareOnLongPress], + ) return { isExternal, @@ -202,14 +214,16 @@ export function Link({ to, action = 'push', onPress: outerOnPress, + onLongPress: outerOnLongPress, download, ...rest }: LinkProps) { - const {href, isExternal, onPress} = useLink({ + const {href, isExternal, onPress, onLongPress} = useLink({ to, displayText: typeof children === 'string' ? children : '', action, onPress: outerOnPress, + onLongPress: outerOnLongPress, }) return ( @@ -220,6 +234,7 @@ export function Link({ accessibilityRole="link" href={href} onPress={download ? undefined : onPress} + onLongPress={onLongPress} {...web({ hrefAttrs: { target: download ? undefined : isExternal ? 'blank' : undefined, @@ -241,7 +256,7 @@ export type InlineLinkProps = React.PropsWithChildren< TextStyleProp & Pick<TextProps, 'selectable' | 'numberOfLines'> > & - Pick<ButtonProps, 'label'> & { + Pick<ButtonProps, 'label' | 'accessibilityHint'> & { disableUnderline?: boolean title?: TextProps['title'] } @@ -253,6 +268,7 @@ export function InlineLinkText({ disableMismatchWarning, style, onPress: outerOnPress, + onLongPress: outerOnLongPress, download, selectable, label, @@ -268,6 +284,7 @@ export function InlineLinkText({ action, disableMismatchWarning, onPress: outerOnPress, + onLongPress: outerOnLongPress, shareOnLongPress, }) const { @@ -319,6 +336,21 @@ export function InlineLinkText({ ) } +export function WebOnlyInlineLinkText({ + children, + to, + onPress, + ...props +}: Omit<InlineLinkProps, 'onLongPress'>) { + return isWeb ? ( + <InlineLinkText {...props} to={to} onPress={onPress}> + {children} + </InlineLinkText> + ) : ( + <Text {...props}>{children}</Text> + ) +} + /** * Utility to create a static `onPress` handler for a `Link` that would otherwise link to a URI * @@ -327,7 +359,10 @@ export function InlineLinkText({ */ export function createStaticClick( onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>, -): Pick<BaseLinkProps, 'to' | 'onPress'> { +): { + to: BaseLinkProps['to'] + onPress: Exclude<BaseLinkProps['onPress'], undefined> +} { return { to: '#', onPress(e: GestureResponderEvent) { @@ -338,17 +373,72 @@ export function createStaticClick( } } -export function WebOnlyInlineLinkText({ - children, - to, - onPress, - ...props -}: InlineLinkProps) { - return isWeb ? ( - <InlineLinkText {...props} to={to} onPress={onPress}> - {children} - </InlineLinkText> - ) : ( - <Text {...props}>{children}</Text> +/** + * Utility to create a static `onPress` handler for a `Link`, but only if the + * click was not modified in some way e.g. `Cmd` or a middle click. + * + * On native, this behaves the same as `createStaticClick` because there are no + * options to "modify" the click in this sense. + * + * Example: + * `<Link {...createStaticClick(e => {...})} />` + */ +export function createStaticClickIfUnmodified( + onPressHandler: Exclude<BaseLinkProps['onPress'], undefined>, +): {onPress: Exclude<BaseLinkProps['onPress'], undefined>} { + return { + onPress(e: GestureResponderEvent) { + if (!isWeb || !isModifiedClickEvent(e)) { + e.preventDefault() + onPressHandler(e) + return false + } + }, + } +} + +/** + * Determines if the click event has a meta key pressed, indicating the user + * intends to deviate from default behavior. + */ +export function isClickEventWithMetaKey(e: GestureResponderEvent) { + if (!isWeb) return false + const event = e as unknown as MouseEvent + return event.metaKey || event.altKey || event.ctrlKey || event.shiftKey +} + +/** + * Determines if the web click target is anything other than `_self` + */ +export function isClickTargetExternal(e: GestureResponderEvent) { + if (!isWeb) return false + const event = e as unknown as MouseEvent + const el = event.currentTarget as HTMLAnchorElement + return el && el.target && el.target !== '_self' +} + +/** + * Determines if a click event has been modified in some way from its default + * behavior, e.g. `Cmd` or a middle click. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} + */ +export function isModifiedClickEvent(e: GestureResponderEvent): boolean { + if (!isWeb) return false + const event = e as unknown as MouseEvent + const isPrimaryButton = event.button === 0 + return ( + isClickEventWithMetaKey(e) || isClickTargetExternal(e) || !isPrimaryButton ) } + +/** + * Determines if a click event has been modified in a way that should indiciate + * that the user intends to open a new tab. + * {@link https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button} + */ +export function shouldClickOpenNewTab(e: GestureResponderEvent) { + if (!isWeb) return false + const event = e as unknown as MouseEvent + const isMiddleClick = isWeb && event.button === 1 + return isClickEventWithMetaKey(e) || isClickTargetExternal(e) || isMiddleClick +} diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx index 99fb2d127e..9c970b051f 100644 --- a/src/components/Menu/index.tsx +++ b/src/components/Menu/index.tsx @@ -47,7 +47,12 @@ export function Root({ return <Context.Provider value={context}>{children}</Context.Provider> } -export function Trigger({children, label, role = 'button'}: TriggerProps) { +export function Trigger({ + children, + label, + role = 'button', + hint, +}: TriggerProps) { const context = useMenuContext() const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() const { @@ -65,11 +70,13 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) { pressed, }, props: { + ref: null, onPress: context.control.open, onFocus, onBlur, onPressIn, onPressOut, + accessibilityHint: hint, accessibilityLabel: label, accessibilityRole: role, }, diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index d1863e478e..dc91161682 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -110,7 +110,12 @@ const RadixTriggerPassThrough = React.forwardRef( ) RadixTriggerPassThrough.displayName = 'RadixTriggerPassThrough' -export function Trigger({children, label, role = 'button'}: TriggerProps) { +export function Trigger({ + children, + label, + role = 'button', + hint, +}: TriggerProps) { const {control} = useMenuContext() const { state: hovered, @@ -153,6 +158,7 @@ export function Trigger({children, label, role = 'button'}: TriggerProps) { onBlur: onBlur, onMouseEnter, onMouseLeave, + accessibilityHint: hint, accessibilityLabel: label, accessibilityRole: role, }, diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts index 44171d42cb..51baa24df5 100644 --- a/src/components/Menu/types.ts +++ b/src/components/Menu/types.ts @@ -19,6 +19,7 @@ export type ItemContextType = { } export type RadixPassThroughTriggerProps = { + ref: React.RefObject<any> id: string type: 'button' disabled: boolean @@ -37,6 +38,7 @@ export type RadixPassThroughTriggerProps = { export type TriggerProps = { children(props: TriggerChildProps): React.ReactNode label: string + hint?: string role?: AccessibilityRole } export type TriggerChildProps = @@ -59,11 +61,13 @@ export type TriggerChildProps = * object is empty. */ props: { + ref: null onPress: () => void onFocus: () => void onBlur: () => void onPressIn: () => void onPressOut: () => void + accessibilityHint?: string accessibilityLabel: string accessibilityRole: AccessibilityRole } @@ -85,6 +89,7 @@ export type TriggerChildProps = onBlur: () => void onMouseEnter: () => void onMouseLeave: () => void + accessibilityHint?: string accessibilityLabel: string accessibilityRole: AccessibilityRole } diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 4edd9f88ee..7005d07428 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,19 +1,13 @@ import React from 'react' import {TextStyle} from 'react-native' import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useNavigation} from '@react-navigation/native' -import {NavigationProp} from '#/lib/routes/types' import {toShortUrl} from '#/lib/strings/url-helpers' -import {isNative} from '#/platform/detection' -import {atoms as a, flatten, native, TextStyleProp, useTheme, web} from '#/alf' +import {atoms as a, flatten, TextStyleProp} from '#/alf' import {isOnlyEmoji} from '#/alf/typography' -import {useInteractionState} from '#/components/hooks/useInteractionState' import {InlineLinkText, LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' -import {TagMenu, useTagMenuControl} from '#/components/TagMenu' +import {RichTextTag} from '#/components/RichTextTag' import {Text, TextProps} from '#/components/Typography' const WORD_WRAP = {wordWrap: 1} @@ -149,10 +143,9 @@ export function RichText({ els.push( <RichTextTag key={key} - text={segment.text} + display={segment.text} tag={tag.tag} - style={interactiveStyles} - selectable={selectable} + textStyle={interactiveStyles} authorHandle={authorHandle} />, ) @@ -177,82 +170,3 @@ export function RichText({ </Text> ) } - -function RichTextTag({ - text, - tag, - style, - selectable, - authorHandle, -}: { - text: string - tag: string - selectable?: boolean - authorHandle?: string -} & TextStyleProp) { - const t = useTheme() - const {_} = useLingui() - const control = useTagMenuControl() - const { - state: hovered, - onIn: onHoverIn, - onOut: onHoverOut, - } = useInteractionState() - const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() - const navigation = useNavigation<NavigationProp>() - - const navigateToPage = React.useCallback(() => { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - }) - }, [navigation, tag]) - - const openDialog = React.useCallback(() => { - control.open() - }, [control]) - - /* - * N.B. On web, this is wrapped in another pressable comopnent with a11y - * labels, etc. That's why only some of these props are applied here. - */ - - return ( - <React.Fragment> - <TagMenu control={control} tag={tag} authorHandle={authorHandle}> - <Text - emoji - selectable={selectable} - {...native({ - accessibilityLabel: _(msg`Hashtag: #${tag}`), - accessibilityHint: _(msg`Long press to open tag menu for #${tag}`), - accessibilityRole: isNative ? 'button' : undefined, - onPress: navigateToPage, - onLongPress: openDialog, - })} - {...web({ - onMouseEnter: onHoverIn, - onMouseLeave: onHoverOut, - })} - // @ts-ignore - onFocus={onFocus} - onBlur={onBlur} - style={[ - web({ - cursor: 'pointer', - }), - {color: t.palette.primary_500}, - (hovered || focused) && { - ...web({ - outline: 0, - textDecorationLine: 'underline', - textDecorationColor: t.palette.primary_500, - }), - }, - style, - ]}> - {text} - </Text> - </TagMenu> - </React.Fragment> - ) -} diff --git a/src/components/RichTextTag.tsx b/src/components/RichTextTag.tsx new file mode 100644 index 0000000000..562d44aa6f --- /dev/null +++ b/src/components/RichTextTag.tsx @@ -0,0 +1,160 @@ +import React from 'react' +import {StyleProp, Text as RNText, TextStyle} from 'react-native' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {NavigationProp} from '#/lib/routes/types' +import {isInvalidHandle} from '#/lib/strings/handles' +import {isNative, isWeb} from '#/platform/detection' +import { + usePreferencesQuery, + useRemoveMutedWordsMutation, + useUpsertMutedWordsMutation, +} from '#/state/queries/preferences' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' +import { + createStaticClick, + createStaticClickIfUnmodified, + InlineLinkText, +} from '#/components/Link' +import {Loader} from '#/components/Loader' +import * as Menu from '#/components/Menu' + +export function RichTextTag({ + tag, + display, + authorHandle, + textStyle, +}: { + tag: string + display: string + authorHandle?: string + textStyle: StyleProp<TextStyle> +}) { + const {_} = useLingui() + const {isLoading: isPreferencesLoading, data: preferences} = + usePreferencesQuery() + const { + mutateAsync: upsertMutedWord, + variables: optimisticUpsert, + reset: resetUpsert, + } = useUpsertMutedWordsMutation() + const { + mutateAsync: removeMutedWords, + variables: optimisticRemove, + reset: resetRemove, + } = useRemoveMutedWordsMutation() + const navigation = useNavigation<NavigationProp>() + const label = _(msg`Hashtag ${tag}`) + const hint = isNative + ? _(msg`Long press to open tag menu for #${tag}`) + : _(msg`Click to open tag menu for ${tag}`) + + const isMuted = Boolean( + (preferences?.moderationPrefs.mutedWords?.find( + m => m.value === tag && m.targets.includes('tag'), + ) ?? + optimisticUpsert?.find( + m => m.value === tag && m.targets.includes('tag'), + )) && + !optimisticRemove?.find(m => m?.value === tag), + ) + + /* + * Mute word records that exactly match the tag in question. + */ + const removeableMuteWords = React.useMemo(() => { + return ( + preferences?.moderationPrefs.mutedWords?.filter(word => { + return word.value === tag + }) || [] + ) + }, [tag, preferences?.moderationPrefs?.mutedWords]) + + return ( + <Menu.Root> + <Menu.Trigger label={label} hint={hint}> + {({props: menuProps}) => ( + <InlineLinkText + to={{ + screen: 'Hashtag', + params: {tag: encodeURIComponent(tag)}, + }} + {...menuProps} + onPress={e => { + if (isWeb) { + return createStaticClickIfUnmodified(() => { + if (!isNative) { + menuProps.onPress() + } + }).onPress(e) + } + }} + onLongPress={createStaticClick(menuProps.onPress).onPress} + accessibilityHint={hint} + label={label} + style={textStyle}> + {isNative ? ( + display + ) : ( + <RNText ref={menuProps.ref}>{display}</RNText> + )} + </InlineLinkText> + )} + </Menu.Trigger> + <Menu.Outer> + <Menu.Group> + <Menu.Item + label={_(msg`See ${tag} posts`)} + onPress={() => { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + }) + }}> + <Menu.ItemText> + <Trans>See #{tag} posts</Trans> + </Menu.ItemText> + <Menu.ItemIcon icon={Search} /> + </Menu.Item> + {authorHandle && !isInvalidHandle(authorHandle) && ( + <Menu.Item + label={_(msg`See ${tag} posts by user`)} + onPress={() => { + navigation.push('Hashtag', { + tag: encodeURIComponent(tag), + author: authorHandle, + }) + }}> + <Menu.ItemText> + <Trans>See #{tag} posts by user</Trans> + </Menu.ItemText> + <Menu.ItemIcon icon={Person} /> + </Menu.Item> + )} + </Menu.Group> + <Menu.Divider /> + <Menu.Item + label={isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)} + onPress={() => { + if (isMuted) { + resetUpsert() + removeMutedWords(removeableMuteWords) + } else { + resetRemove() + upsertMutedWord([ + {value: tag, targets: ['tag'], actorTarget: 'all'}, + ]) + } + }}> + <Menu.ItemText> + {isMuted ? _(msg`Unmute ${tag}`) : _(msg`Mute ${tag}`)} + </Menu.ItemText> + <Menu.ItemIcon icon={isPreferencesLoading ? Loader : Mute} /> + </Menu.Item> + </Menu.Outer> + </Menu.Root> + ) +} diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx index 64307070bf..d8925684b6 100644 --- a/src/components/StarterPack/ProfileStarterPacks.tsx +++ b/src/components/StarterPack/ProfileStarterPacks.tsx @@ -136,7 +136,6 @@ export const ProfileStarterPacks = React.forwardRef< headerOffset={headerOffset} progressViewOffset={ios(0)} contentContainerStyle={{paddingBottom: headerOffset + bottomBarOffset}} - indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} desktopFixedHeight onEndReached={onEndReached} diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx deleted file mode 100644 index 310ecc4c20..0000000000 --- a/src/components/TagMenu/index.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import React from 'react' -import {View} from 'react-native' -import {msg, Trans} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useNavigation} from '@react-navigation/native' - -import {NavigationProp} from '#/lib/routes/types' -import {isInvalidHandle} from '#/lib/strings/handles' -import { - usePreferencesQuery, - useRemoveMutedWordsMutation, - useUpsertMutedWordsMutation, -} from '#/state/queries/preferences' -import {atoms as a, native, useTheme} from '#/alf' -import {Button, ButtonText} from '#/components/Button' -import * as Dialog from '#/components/Dialog' -import {Divider} from '#/components/Divider' -import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' -import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' -import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person' -import {createStaticClick, Link} from '#/components/Link' -import {Loader} from '#/components/Loader' -import {Text} from '#/components/Typography' - -export function useTagMenuControl() { - return Dialog.useDialogControl() -} - -export function TagMenu({ - children, - control, - tag, - authorHandle, -}: React.PropsWithChildren<{ - control: Dialog.DialogOuterProps['control'] - /** - * This should be the sanitized tag value from the facet itself, not the - * "display" value with a leading `#`. - */ - tag: string - authorHandle?: string -}>) { - const navigation = useNavigation<NavigationProp>() - return ( - <> - {children} - <Dialog.Outer control={control}> - <Dialog.Handle /> - <TagMenuInner - control={control} - tag={tag} - authorHandle={authorHandle} - navigation={navigation} - /> - </Dialog.Outer> - </> - ) -} - -function TagMenuInner({ - control, - tag, - authorHandle, - navigation, -}: { - control: Dialog.DialogOuterProps['control'] - tag: string - authorHandle?: string - // Passed down because on native, we don't use real portals (and context would be wrong). - navigation: NavigationProp -}) { - const {_} = useLingui() - const t = useTheme() - const {isLoading: isPreferencesLoading, data: preferences} = - usePreferencesQuery() - const { - mutateAsync: upsertMutedWord, - variables: optimisticUpsert, - reset: resetUpsert, - } = useUpsertMutedWordsMutation() - const { - mutateAsync: removeMutedWords, - variables: optimisticRemove, - reset: resetRemove, - } = useRemoveMutedWordsMutation() - const displayTag = '#' + tag - - const isMuted = Boolean( - (preferences?.moderationPrefs.mutedWords?.find( - m => m.value === tag && m.targets.includes('tag'), - ) ?? - optimisticUpsert?.find( - m => m.value === tag && m.targets.includes('tag'), - )) && - !optimisticRemove?.find(m => m?.value === tag), - ) - - /* - * Mute word records that exactly match the tag in question. - */ - const removeableMuteWords = React.useMemo(() => { - return ( - preferences?.moderationPrefs.mutedWords?.filter(word => { - return word.value === tag - }) || [] - ) - }, [tag, preferences?.moderationPrefs?.mutedWords]) - - return ( - <Dialog.Inner label={_(msg`Tag menu: ${displayTag}`)}> - {isPreferencesLoading ? ( - <View style={[a.w_full, a.align_center]}> - <Loader size="lg" /> - </View> - ) : ( - <> - <View - style={[ - a.rounded_md, - a.border, - a.mb_md, - t.atoms.border_contrast_low, - t.atoms.bg_contrast_25, - ]}> - <Link - label={_(msg`View all posts with tag ${displayTag}`)} - {...createStaticClick(() => { - control.close(() => { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - }) - }) - })}> - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_start, - a.gap_md, - a.px_lg, - a.py_md, - ]}> - <Search size="lg" style={[t.atoms.text_contrast_medium]} /> - <Text - numberOfLines={1} - ellipsizeMode="middle" - style={[ - a.flex_1, - a.text_md, - a.font_bold, - native({top: 2}), - t.atoms.text_contrast_medium, - ]}> - <Trans> - See{' '} - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {displayTag} - </Text>{' '} - posts - </Trans> - </Text> - </View> - </Link> - - {authorHandle && !isInvalidHandle(authorHandle) && ( - <> - <Divider /> - - <Link - label={_( - msg`View all posts by @${authorHandle} with tag ${displayTag}`, - )} - {...createStaticClick(() => { - control.close(() => { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - author: authorHandle, - }) - }) - })}> - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_start, - a.gap_md, - a.px_lg, - a.py_md, - ]}> - <Person size="lg" style={[t.atoms.text_contrast_medium]} /> - <Text - numberOfLines={1} - ellipsizeMode="middle" - style={[ - a.flex_1, - a.text_md, - a.font_bold, - native({top: 2}), - t.atoms.text_contrast_medium, - ]}> - <Trans> - See{' '} - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {displayTag} - </Text>{' '} - posts by this user - </Trans> - </Text> - </View> - </Link> - </> - )} - - {preferences ? ( - <> - <Divider /> - - <Button - label={ - isMuted - ? _(msg`Unmute all ${displayTag} posts`) - : _(msg`Mute all ${displayTag} posts`) - } - onPress={() => { - control.close(() => { - if (isMuted) { - resetUpsert() - removeMutedWords(removeableMuteWords) - } else { - resetRemove() - upsertMutedWord([ - { - value: tag, - targets: ['tag'], - actorTarget: 'all', - }, - ]) - } - }) - }}> - <View - style={[ - a.w_full, - a.flex_row, - a.align_center, - a.justify_start, - a.gap_md, - a.px_lg, - a.py_md, - ]}> - <Mute size="lg" style={[t.atoms.text_contrast_medium]} /> - <Text - numberOfLines={1} - ellipsizeMode="middle" - style={[ - a.flex_1, - a.text_md, - a.font_bold, - native({top: 2}), - t.atoms.text_contrast_medium, - ]}> - {isMuted ? _(msg`Unmute`) : _(msg`Mute`)}{' '} - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> - {displayTag} - </Text>{' '} - <Trans>posts</Trans> - </Text> - </View> - </Button> - </> - ) : null} - </View> - - <Button - label={_(msg`Close this dialog`)} - size="small" - variant="ghost" - color="secondary" - onPress={() => control.close()}> - <ButtonText> - <Trans>Cancel</Trans> - </ButtonText> - </Button> - </> - )} - </Dialog.Inner> - ) -} diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx deleted file mode 100644 index b6c306439a..0000000000 --- a/src/components/TagMenu/index.web.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React from 'react' -import {msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useNavigation} from '@react-navigation/native' - -import {NavigationProp} from '#/lib/routes/types' -import {isInvalidHandle} from '#/lib/strings/handles' -import {enforceLen} from '#/lib/strings/helpers' -import { - usePreferencesQuery, - useRemoveMutedWordsMutation, - useUpsertMutedWordsMutation, -} from '#/state/queries/preferences' -import {EventStopper} from '#/view/com/util/EventStopper' -import {NativeDropdown} from '#/view/com/util/forms/NativeDropdown' -import {web} from '#/alf' -import * as Dialog from '#/components/Dialog' - -export function useTagMenuControl(): Dialog.DialogControlProps { - return { - id: '', - // @ts-ignore - ref: null, - open: () => { - throw new Error(`TagMenu controls are only available on native platforms`) - }, - close: () => { - throw new Error(`TagMenu controls are only available on native platforms`) - }, - } -} - -export function TagMenu({ - children, - tag, - authorHandle, -}: React.PropsWithChildren<{ - /** - * This should be the sanitized tag value from the facet itself, not the - * "display" value with a leading `#`. - */ - tag: string - authorHandle?: string -}>) { - const {_} = useLingui() - const navigation = useNavigation<NavigationProp>() - const {data: preferences} = usePreferencesQuery() - const {mutateAsync: upsertMutedWord, variables: optimisticUpsert} = - useUpsertMutedWordsMutation() - const {mutateAsync: removeMutedWords, variables: optimisticRemove} = - useRemoveMutedWordsMutation() - const isMuted = Boolean( - (preferences?.moderationPrefs.mutedWords?.find( - m => m.value === tag && m.targets.includes('tag'), - ) ?? - optimisticUpsert?.find( - m => m.value === tag && m.targets.includes('tag'), - )) && - !optimisticRemove?.find(m => m?.value === tag), - ) - const truncatedTag = '#' + enforceLen(tag, 15, true, 'middle') - - /* - * Mute word records that exactly match the tag in question. - */ - const removeableMuteWords = React.useMemo(() => { - return ( - preferences?.moderationPrefs.mutedWords?.filter(word => { - return word.value === tag - }) || [] - ) - }, [tag, preferences?.moderationPrefs?.mutedWords]) - - const dropdownItems = React.useMemo(() => { - return [ - { - label: _(msg`See ${truncatedTag} posts`), - onPress() { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - }) - }, - testID: 'tagMenuSearch', - icon: { - ios: { - name: 'magnifyingglass', - }, - android: '', - web: 'magnifying-glass', - }, - }, - authorHandle && - !isInvalidHandle(authorHandle) && { - label: _(msg`See ${truncatedTag} posts by user`), - onPress() { - navigation.push('Hashtag', { - tag: encodeURIComponent(tag), - author: authorHandle, - }) - }, - testID: 'tagMenuSearchByUser', - icon: { - ios: { - name: 'magnifyingglass', - }, - android: '', - web: ['far', 'user'], - }, - }, - preferences && { - label: 'separator', - }, - preferences && { - label: isMuted - ? _(msg`Unmute ${truncatedTag}`) - : _(msg`Mute ${truncatedTag}`), - onPress() { - if (isMuted) { - removeMutedWords(removeableMuteWords) - } else { - upsertMutedWord([ - {value: tag, targets: ['tag'], actorTarget: 'all'}, - ]) - } - }, - testID: 'tagMenuMute', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_menu_sort_alphabetically', - web: isMuted ? 'eye' : ['far', 'eye-slash'], - }, - }, - ].filter(Boolean) - }, [ - _, - authorHandle, - isMuted, - navigation, - preferences, - tag, - truncatedTag, - upsertMutedWord, - removeMutedWords, - removeableMuteWords, - ]) - - return ( - <EventStopper> - <NativeDropdown - accessibilityLabel={_(msg`Click here to open tag menu for ${tag}`)} - accessibilityHint="" - // @ts-ignore - items={dropdownItems} - triggerStyle={web({ - textAlign: 'left', - })}> - {children} - </NativeDropdown> - </EventStopper> - ) -} diff --git a/src/lib/bitdrift.ts b/src/lib/bitdrift.ts index f11da6f3b2..71493d0bcf 100644 --- a/src/lib/bitdrift.ts +++ b/src/lib/bitdrift.ts @@ -1,27 +1,23 @@ -// import {init} from '@bitdrift/react-native' -// import {Statsig} from 'statsig-react-native-expo' -// export {debug, error, info, warn} from '@bitdrift/react-native' +import {init, SessionStrategy} from '@bitdrift/react-native' +import {Statsig} from 'statsig-react-native-expo' +export {debug, error, info, warn} from '@bitdrift/react-native' -// import {initPromise} from './statsig/statsig' +import {initPromise} from './statsig/statsig' -// const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY +const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY -// initPromise.then(() => { -// let isEnabled = false -// try { -// if (Statsig.checkGate('enable_bitdrift')) { -// isEnabled = true -// } -// } catch (e) { -// // Statsig may complain about it being called too early. -// } -// if (isEnabled && BITDRIFT_API_KEY) { -// init(BITDRIFT_API_KEY, {url: 'https://api-bsky.bitdrift.io'}) -// } -// }) - -// TODO: Reenable when the build issue is fixed. -export function debug(_message: string) {} -export function error(_message: string) {} -export function info(_message: string) {} -export function warn(_message: string) {} +initPromise.then(() => { + let isEnabled = false + try { + if (Statsig.checkGate('enable_bitdrift')) { + isEnabled = true + } + } catch (e) { + // Statsig may complain about it being called too early. + } + if (isEnabled && BITDRIFT_API_KEY) { + init(BITDRIFT_API_KEY, SessionStrategy.Activity, { + url: 'https://api-bsky.bitdrift.io', + }) + } +}) diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 945e61c994..aa7ff29286 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -139,6 +139,11 @@ export const TIMELINE_SAVED_FEED = { value: 'following', pinned: true, } +export const VIDEO_SAVED_FEED = { + type: 'feed', + value: VIDEO_FEED_URI, + pinned: true, +} export const RECOMMENDED_SAVED_FEEDS: Pick< AppBskyActorDefs.SavedFeed, diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index d2caa47f2a..c0508c2dd0 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -3,6 +3,7 @@ export type Gate = | 'debug_show_feedcontext' | 'debug_subscriptions' | 'new_postonboarding' + | 'onboarding_add_video_feed' | 'remove_show_latest_button' | 'test_gate_1' | 'test_gate_2' diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index e59196f669..e0882806d5 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -5,8 +5,7 @@ import {sha256} from 'js-sha256' import {Statsig, StatsigProvider} from 'statsig-react-native-expo' import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info' -// TODO: Reenable when the build issue is fixed. -// import * as bitdrift from '#/lib/bitdrift' +import * as bitdrift from '#/lib/bitdrift' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import * as persisted from '#/state/persisted' @@ -108,8 +107,7 @@ export function logEvent<E extends keyof LogEvents>( console.groupCollapsed(eventName) console.log(fullMetadata) console.groupEnd() - // TODO: Reenable when the build issue is fixed. - // bitdrift.info(eventName, fullMetadata) + bitdrift.info(eventName, fullMetadata) } catch (e) { // A log should never interrupt the calling code, whatever happens. logger.error('Failed to log an event', {message: e}) diff --git a/src/platform/urls.tsx b/src/platform/urls.tsx index fd9d297aa2..514bde43e2 100644 --- a/src/platform/urls.tsx +++ b/src/platform/urls.tsx @@ -1,4 +1,4 @@ -import {GestureResponderEvent, Linking} from 'react-native' +import {Linking} from 'react-native' import {isNative, isWeb} from './detection' @@ -24,15 +24,3 @@ export function clearHash() { window.location.hash = '' } } - -export function shouldClickOpenNewTab(e: GestureResponderEvent) { - /** - * A `GestureResponderEvent`, but cast to `any` to avoid using a bunch - * of @ts-ignore below. - */ - const event = e as any - const isMiddleClick = isWeb && event.button === 1 - const isMetaKey = - isWeb && (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) - return isMetaKey || isMiddleClick -} diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index fc0ea6a247..587ffa4f71 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -12,6 +12,7 @@ import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, TIMELINE_SAVED_FEED, + VIDEO_SAVED_FEED, } from '#/lib/constants' import {useRequestNotificationsPermission} from '#/lib/notifications/notifications' import {logEvent, useGate} from '#/lib/statsig/statsig' @@ -111,6 +112,12 @@ export function StepFinished() { id: TID.nextStr(), }, ] + if (gate('onboarding_add_video_feed')) { + feedsToSave.push({ + ...VIDEO_SAVED_FEED, + id: TID.nextStr(), + }) + } // Any starter pack feeds will be pinned _after_ the defaults if (starterPack && starterPack.feeds?.length) { diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index a591488891..b0b608f172 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -17,7 +17,6 @@ import {usePalette} from '#/lib/hooks/usePalette' import {sanitizeHandle} from '#/lib/strings/handles' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {shouldClickOpenNewTab} from '#/platform/urls' import {FeedSourceInfo, useFeedSourceInfoQuery} from '#/state/queries/feed' import { useAddSavedFeedsMutation, @@ -29,6 +28,7 @@ import {FeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import * as Toast from '#/view/com/util/Toast' import {useTheme} from '#/alf' import {atoms as a} from '#/alf' +import {shouldClickOpenNewTab} from '#/components/Link' import * as Prompt from '#/components/Prompt' import {RichText} from '#/components/RichText' import {Text} from '../util/text/Text' diff --git a/src/view/com/feeds/ProfileFeedgens.tsx b/src/view/com/feeds/ProfileFeedgens.tsx index 64705ded8e..f5894f9ee7 100644 --- a/src/view/com/feeds/ProfileFeedgens.tsx +++ b/src/view/com/feeds/ProfileFeedgens.tsx @@ -205,9 +205,7 @@ export const ProfileFeedgens = React.forwardRef< headerOffset={headerOffset} progressViewOffset={ios(0)} contentContainerStyle={isMobile && {paddingBottom: headerOffset + 100}} - indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} - // @ts-ignore our .web version only -prf desktopFixedHeight onEndReached={onEndReached} /> diff --git a/src/view/com/lists/ProfileLists.tsx b/src/view/com/lists/ProfileLists.tsx index 2f63fd172b..d91a4fb669 100644 --- a/src/view/com/lists/ProfileLists.tsx +++ b/src/view/com/lists/ProfileLists.tsx @@ -203,9 +203,7 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>( contentContainerStyle={ isMobile && {paddingBottom: headerOffset + 100} } - indicatorStyle={t.name === 'light' ? 'black' : 'white'} removeClippedSubviews={true} - // @ts-ignore our .web version only -prf desktopFixedHeight onEndReached={onEndReached} /> diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index 888bba4bf4..4a68508354 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -19,7 +19,6 @@ import {isVideoView} from '#/lib/embeds' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries' import {logEvent} from '#/lib/statsig/statsig' -import {useTheme} from '#/lib/ThemeContext' import {logger} from '#/logger' import {isIOS, isNative, isWeb} from '#/platform/detection' import {listenPostCreated} from '#/state/events' @@ -188,7 +187,6 @@ let PostFeed = ({ initialNumToRender?: number isVideoFeed?: boolean }): React.ReactNode => { - const theme = useTheme() const {_} = useLingui() const queryClient = useQueryClient() const {currentAccount, hasSession} = useSession() @@ -716,7 +714,6 @@ let PostFeed = ({ minHeight: Dimensions.get('window').height * 1.5, }} onScrolledDownChange={onScrolledDownChange} - indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={2} // number of posts left to trigger load more removeClippedSubviews={true} diff --git a/src/view/com/util/List.tsx b/src/view/com/util/List.tsx index 41ca5b5725..28f21d0392 100644 --- a/src/view/com/util/List.tsx +++ b/src/view/com/util/List.tsx @@ -164,6 +164,7 @@ let List = React.forwardRef<ListMethods, ListProps>( right: 1, ...props.scrollIndicatorInsets, }} + indicatorStyle={t.scheme === 'dark' ? 'white' : 'black'} contentOffset={contentOffset} refreshControl={refreshControl} onScroll={scrollHandler} diff --git a/yarn.lock b/yarn.lock index c212f8f07c..b462e66574 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3422,6 +3422,14 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@bitdrift/react-native@^0.6.2": + version "0.6.2" + resolved "https://registry.yarnpkg.com/@bitdrift/react-native/-/react-native-0.6.2.tgz#8e75d45a63fccad38b310fdea8069fa929cb97c3" + integrity sha512-4DIsZwAr9/Q1RI7lsnUphRoMuOuLWWESNXI759niSmU8XHTJISwwOQzUm7qWn7waBJGhxaq+jn+vlTV5Fai6zw== + dependencies: + "@expo/config-plugins" "^9.0.14" + fast-json-stringify "^6.0.0" + "@braintree/sanitize-url@^6.0.2": version "6.0.4" resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783" @@ -3838,6 +3846,26 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@^9.0.14": + version "9.0.14" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.14.tgz#c57cc86c238b276823ff66d96e4722366d57b12c" + integrity sha512-Lx1ebV95rTFKKQmbu4wMPLz65rKn7mqSpfANdCx+KwRxuLY2JQls8V4h3lQjG6dW8NWf9qV5QaEFAgNB6VMyOQ== + dependencies: + "@expo/config-types" "^52.0.3" + "@expo/json-file" "~9.0.1" + "@expo/plist" "^0.2.1" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.5" + getenv "^1.0.0" + glob "^10.4.2" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + "@expo/config-plugins@~9.0.12": version "9.0.12" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.12.tgz#f122b2dca22e135eadf6e73442da3ced0ce8aa0a" @@ -3863,6 +3891,11 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.1.tgz#327af1b72a3a9d4556f41e083e0e284dd8198b96" integrity sha512-vD8ZetyKV7U29lR6+NJohYeoLYTH+eNYXJeNiSOrWCz0witJYY11meMmEnpEaVbN89EfC6uauSUOa6wihtbyPQ== +"@expo/config-types@^52.0.3": + version "52.0.3" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.3.tgz#511f2f868172c93abeac7183beeb921dc72d6e1e" + integrity sha512-muxvuARmbysH5OGaiBRlh1Y6vfdmL56JtpXxB+y2Hfhu0ezG1U4FjZYBIacthckZPvnDCcP3xIu1R+eTo7/QFA== + "@expo/config@~10.0.4": version "10.0.5" resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.5.tgz#2de75e3f5d46a55f9f5140b73e0913265e6a41c6" @@ -3986,6 +4019,15 @@ json5 "^2.2.3" write-file-atomic "^2.3.0" +"@expo/json-file@~9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.1.tgz#ff60654caf1fa3c33f9b17dcd1e9691eb854a318" + integrity sha512-ZVPhbbEBEwafPCJ0+kI25O2Iivt3XKHEKAADCml1q2cmOIbQnKgLyn8DpOJXqWEyRQr/VWS+hflBh8DU2YFSqg== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + write-file-atomic "^2.3.0" + "@expo/metro-config@0.19.8", "@expo/metro-config@~0.19.8": version "0.19.8" resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.8.tgz#f1ea552b6fa5217093fe364ff5ca78a7e261a28b" @@ -4045,6 +4087,15 @@ base64-js "^1.2.3" xmlbuilder "^14.0.0" +"@expo/plist@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.1.tgz#a315e1964ee9eece5c56040d460db5de7af85889" + integrity sha512-9TaXGuNxa0LQwHQn4rYiU6YaERv6dPnQgsdKWq2rKKTr6LWOtGNQCi/yOk/HBLeZSxBm59APT5/6x60uRvr0Mg== + dependencies: + "@xmldom/xmldom" "~0.7.7" + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + "@expo/prebuild-config@^8.0.23": version "8.0.23" resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-8.0.23.tgz#2ec6d5464f35d308bdb94ba75b7e6aba0ebb507d" @@ -4140,6 +4191,13 @@ resolved "https://registry.yarnpkg.com/@fastify/deepmerge/-/deepmerge-1.3.0.tgz#8116858108f0c7d9fd460d05a7d637a13fe3239a" integrity sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A== +"@fastify/merge-json-schemas@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz#3aa30d2f0c81a8ac5995b6d94ed4eaa2c3055824" + integrity sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A== + dependencies: + dequal "^2.0.3" + "@floating-ui/core@^0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.7.3.tgz#d274116678ffae87f6b60e90f88cc4083eefab86" @@ -7542,6 +7600,13 @@ ajv-formats@^2.1.1: dependencies: ajv "^8.0.0" +ajv-formats@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578" + integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ== + dependencies: + ajv "^8.0.0" + ajv-keywords@^3.5.2: version "3.5.2" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" @@ -7584,6 +7649,16 @@ ajv@^8.0.0, ajv@^8.10.0, ajv@^8.11.0, ajv@^8.9.0: require-from-string "^2.0.2" uri-js "^4.2.2" +ajv@^8.12.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -9542,6 +9617,11 @@ deprecated-react-native-prop-types@^5.0.0: invariant "^2.2.4" prop-types "^15.8.1" +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -10856,6 +10936,18 @@ fast-json-stringify@^5.8.0: fast-uri "^2.1.0" rfdc "^1.2.0" +fast-json-stringify@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz#82f1cb45fa96d0ca24b601f1738066976d6e2430" + integrity sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg== + dependencies: + "@fastify/merge-json-schemas" "^0.2.0" + ajv "^8.12.0" + ajv-formats "^3.0.1" + fast-uri "^3.0.0" + json-schema-ref-resolver "^2.0.0" + rfdc "^1.2.0" + fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" @@ -10888,6 +10980,11 @@ fast-uri@^2.1.0: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-2.2.0.tgz#519a0f849bef714aad10e9753d69d8f758f7445a" integrity sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg== +fast-uri@^3.0.0, fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + fast-xml-parser@4.2.5: version "4.2.5" resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz#a6747a09296a6cb34f2ae634019bf1738f3b421f" @@ -13189,6 +13286,13 @@ json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== +json-schema-ref-resolver@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz#c92f16b452df069daac53e1984159e0f9af0598d" + integrity sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q== + dependencies: + dequal "^2.0.3" + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"