From e721f84a2cd64bd98f54049bd17925ddf1b194c8 Mon Sep 17 00:00:00 2001 From: Hailey Date: Tue, 5 Mar 2024 18:55:23 -0800 Subject: [PATCH 01/29] add `newArchEnabled: false` flag to app.config.js (#3115) --- app.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.config.js b/app.config.js index a18addcce4..9691cab234 100644 --- a/app.config.js +++ b/app.config.js @@ -121,12 +121,14 @@ module.exports = function (config) { { ios: { deploymentTarget: '13.4', + newArchEnabled: false, }, android: { compileSdkVersion: 34, targetSdkVersion: 34, buildToolsVersion: '34.0.0', kotlinVersion: '1.8.0', + newArchEnabled: false, }, }, ], From 317e0cda7a30d21f35229c096b6ef3284819d19a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 5 Mar 2024 21:15:42 -0600 Subject: [PATCH 02/29] Add `Menu` component (#3097) * Add POC menu abstraction * Better platform handling * Remove ignore * Add some menu items * Add controlled dropdown * Pass through a11y props * Ignore uninitialized context * Tweaks * Usability improvements * Rename handlers to props * Add radix comment * Ignore known type * Remove todo * Move storybook item * Improve Group matching * Adjust theming --- bskyweb/templates/base.html | 1 + package.json | 2 + src/components/Dialog/context.ts | 27 +-- src/components/Dialog/types.ts | 1 + src/components/Menu/context.tsx | 8 + src/components/Menu/index.tsx | 190 +++++++++++++++++++++ src/components/Menu/index.web.tsx | 247 +++++++++++++++++++++++++++ src/components/Menu/types.ts | 72 ++++++++ src/view/screens/Storybook/Menus.tsx | 79 +++++++++ src/view/screens/Storybook/index.tsx | 2 + web/index.html | 1 + yarn.lock | 93 ++++++++++ 12 files changed, 712 insertions(+), 11 deletions(-) create mode 100644 src/components/Menu/context.tsx create mode 100644 src/components/Menu/index.tsx create mode 100644 src/components/Menu/index.web.tsx create mode 100644 src/components/Menu/types.ts create mode 100644 src/view/screens/Storybook/Menus.tsx diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html index 413d7ff691..55447552ea 100644 --- a/bskyweb/templates/base.html +++ b/bskyweb/templates/base.html @@ -213,6 +213,7 @@ } /* NativeDropdown component */ + .radix-dropdown-item:focus, .nativeDropdown-item:focus { outline: none; } diff --git a/package.json b/package.json index d694d26c31..cd8cc76519 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "@lingui/react": "^4.5.0", "@mattermost/react-native-paste-input": "^0.6.4", "@miblanchard/react-native-slider": "^2.3.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@react-native-async-storage/async-storage": "1.21.0", "@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-clipboard/clipboard": "^1.10.0", @@ -148,6 +149,7 @@ "react-avatar-editor": "^13.0.0", "react-circular-progressbar": "^2.1.0", "react-dom": "^18.2.0", + "react-keyed-flatten-children": "^3.0.0", "react-native": "0.73.2", "react-native-appstate-hook": "^1.0.6", "react-native-drawer-layout": "^4.0.0-alpha.3", diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index eb717d8e2b..9b571e8e9c 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -21,7 +21,8 @@ export function useDialogControl(): DialogOuterProps['control'] { open: () => {}, close: () => {}, }) - const {activeDialogs} = useDialogStateContext() + const {activeDialogs, openDialogs} = useDialogStateContext() + const isOpen = openDialogs.includes(id) React.useEffect(() => { activeDialogs.current.set(id, control) @@ -31,14 +32,18 @@ export function useDialogControl(): DialogOuterProps['control'] { } }, [id, activeDialogs]) - return { - id, - ref: control, - open: () => { - control.current.open() - }, - close: cb => { - control.current.close(cb) - }, - } + return React.useMemo( + () => ({ + id, + ref: control, + isOpen, + open: () => { + control.current.open() + }, + close: cb => { + control.current.close(cb) + }, + }), + [id, control, isOpen], + ) } diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index 78dfedf5a8..fa9398fe05 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -22,6 +22,7 @@ export type DialogControlRefProps = { export type DialogControlProps = DialogControlRefProps & { id: string ref: React.RefObject + isOpen: boolean } export type DialogContextProps = { diff --git a/src/components/Menu/context.tsx b/src/components/Menu/context.tsx new file mode 100644 index 0000000000..9fc91f6815 --- /dev/null +++ b/src/components/Menu/context.tsx @@ -0,0 +1,8 @@ +import React from 'react' + +import type {ContextType} from '#/components/Menu/types' + +export const Context = React.createContext({ + // @ts-ignore + control: null, +}) diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx new file mode 100644 index 0000000000..ee96a5667e --- /dev/null +++ b/src/components/Menu/index.tsx @@ -0,0 +1,190 @@ +import React from 'react' +import {View, Pressable} from 'react-native' +import flattenReactChildren from 'react-keyed-flatten-children' + +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {Text} from '#/components/Typography' + +import {Context} from '#/components/Menu/context' +import { + ContextType, + TriggerProps, + ItemProps, + GroupProps, + ItemTextProps, + ItemIconProps, +} from '#/components/Menu/types' + +export {useDialogControl as useMenuControl} from '#/components/Dialog' + +export function useMemoControlContext() { + return React.useContext(Context) +} + +export function Root({ + children, + control, +}: React.PropsWithChildren<{ + control?: Dialog.DialogOuterProps['control'] +}>) { + const defaultControl = Dialog.useDialogControl() + const context = React.useMemo( + () => ({ + control: control || defaultControl, + }), + [control, defaultControl], + ) + + return {children} +} + +export function Trigger({children, label}: TriggerProps) { + const {control} = React.useContext(Context) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + return children({ + isNative: true, + control, + state: { + hovered: false, + focused, + pressed, + }, + props: { + onPress: control.open, + onFocus, + onBlur, + onPressIn, + onPressOut, + accessibilityLabel: label, + }, + }) +} + +export function Outer({children}: React.PropsWithChildren<{}>) { + const context = React.useContext(Context) + + return ( + + + + {/* Re-wrap with context since Dialogs are portal-ed to root */} + + + {children} + + + + + ) +} + +export function Item({children, label, style, onPress, ...rest}: ItemProps) { + const t = useTheme() + const {control} = React.useContext(Context) + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + const { + state: pressed, + onIn: onPressIn, + onOut: onPressOut, + } = useInteractionState() + + return ( + { + onPress(e) + + if (!e.defaultPrevented) { + control?.close() + } + }} + onFocus={onFocus} + onBlur={onBlur} + onPressIn={onPressIn} + onPressOut={onPressOut} + style={[ + a.flex_row, + a.align_center, + a.gap_sm, + a.px_md, + a.rounded_md, + a.border, + t.atoms.bg_contrast_25, + t.atoms.border_contrast_low, + {minHeight: 44, paddingVertical: 10}, + style, + (focused || pressed) && [t.atoms.bg_contrast_50], + ]}> + {children} + + ) +} + +export function ItemText({children, style}: ItemTextProps) { + const t = useTheme() + return ( + + {children} + + ) +} + +export function ItemIcon({icon: Comp}: ItemIconProps) { + const t = useTheme() + return +} + +export function Group({children, style}: GroupProps) { + const t = useTheme() + return ( + + {flattenReactChildren(children).map((child, i) => { + return React.isValidElement(child) && child.type === Item ? ( + + {i > 0 ? ( + + ) : null} + {React.cloneElement(child, { + // @ts-ignore + style: { + borderRadius: 0, + borderWidth: 0, + }, + })} + + ) : null + })} + + ) +} + +export function Divider() { + return null +} diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx new file mode 100644 index 0000000000..ca2e40566d --- /dev/null +++ b/src/components/Menu/index.web.tsx @@ -0,0 +1,247 @@ +import React from 'react' +import {View, Pressable} from 'react-native' +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' + +import * as Dialog from '#/components/Dialog' +import {useInteractionState} from '#/components/hooks/useInteractionState' +import {atoms as a, useTheme, flatten, web} from '#/alf' +import {Text} from '#/components/Typography' + +import { + ContextType, + TriggerProps, + ItemProps, + GroupProps, + ItemTextProps, + ItemIconProps, +} from '#/components/Menu/types' +import {Context} from '#/components/Menu/context' + +export function useMenuControl(): Dialog.DialogControlProps { + const id = React.useId() + const [isOpen, setIsOpen] = React.useState(false) + + return React.useMemo( + () => ({ + id, + ref: {current: null}, + isOpen, + open() { + setIsOpen(true) + }, + close() { + setIsOpen(false) + }, + }), + [id, isOpen, setIsOpen], + ) +} + +export function useMemoControlContext() { + return React.useContext(Context) +} + +export function Root({ + children, + control, +}: React.PropsWithChildren<{ + control?: Dialog.DialogOuterProps['control'] +}>) { + const defaultControl = useMenuControl() + const context = React.useMemo( + () => ({ + control: control || defaultControl, + }), + [control, defaultControl], + ) + const onOpenChange = React.useCallback( + (open: boolean) => { + if (context.control.isOpen && !open) { + context.control.close() + } else if (!context.control.isOpen && open) { + context.control.open() + } + }, + [context.control], + ) + + return ( + + + {children} + + + ) +} + +export function Trigger({children, label, style}: TriggerProps) { + const {control} = React.useContext(Context) + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + return ( + + { + control.open() + }} + {...web({ + onMouseEnter, + onMouseLeave, + })}> + {children({ + isNative: false, + control, + state: { + hovered, + focused, + pressed: false, + }, + props: {}, + })} + + + ) +} + +export function Outer({children}: React.PropsWithChildren<{}>) { + const t = useTheme() + + return ( + + + + {children} + + + + + + ) +} + +export function Item({children, label, onPress, ...rest}: ItemProps) { + const t = useTheme() + const {control} = React.useContext(Context) + const { + state: hovered, + onIn: onMouseEnter, + onOut: onMouseLeave, + } = useInteractionState() + const {state: focused, onIn: onFocus, onOut: onBlur} = useInteractionState() + + return ( + + { + onPress(e) + + /** + * Ported forward from Radix + * @see https://www.radix-ui.com/primitives/docs/components/dropdown-menu#item + */ + if (!e.defaultPrevented) { + control.close() + } + }} + onFocus={onFocus} + onBlur={onBlur} + // need `flatten` here for Radix compat + style={flatten([ + a.flex_row, + a.align_center, + a.gap_sm, + a.py_sm, + a.rounded_xs, + {minHeight: 32, paddingHorizontal: 10}, + web({outline: 0}), + (hovered || focused) && [ + web({outline: '0 !important'}), + t.name === 'light' + ? t.atoms.bg_contrast_25 + : t.atoms.bg_contrast_50, + ], + ])} + {...web({ + onMouseEnter, + onMouseLeave, + })}> + {children} + + + ) +} + +export function ItemText({children, style}: ItemTextProps) { + const t = useTheme() + return ( + + {children} + + ) +} + +export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) { + const t = useTheme() + return ( + + ) +} + +export function Group({children}: GroupProps) { + return children +} + +export function Divider() { + const t = useTheme() + return ( + + ) +} diff --git a/src/components/Menu/types.ts b/src/components/Menu/types.ts new file mode 100644 index 0000000000..2f52e63906 --- /dev/null +++ b/src/components/Menu/types.ts @@ -0,0 +1,72 @@ +import React from 'react' +import {GestureResponderEvent, PressableProps} from 'react-native' + +import {Props as SVGIconProps} from '#/components/icons/common' +import * as Dialog from '#/components/Dialog' +import {TextStyleProp, ViewStyleProp} from '#/alf' + +export type ContextType = { + control: Dialog.DialogOuterProps['control'] +} + +export type TriggerProps = ViewStyleProp & { + children(props: TriggerChildProps): React.ReactNode + label: string +} +export type TriggerChildProps = + | { + isNative: true + control: Dialog.DialogOuterProps['control'] + 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 + control: Dialog.DialogOuterProps['control'] + state: { + hovered: boolean + focused: boolean + /** + * Native only, `false` on web + */ + pressed: false + } + props: {} + } + +export type ItemProps = React.PropsWithChildren< + Omit & + ViewStyleProp & { + label: string + onPress: (e: GestureResponderEvent) => void + } +> + +export type ItemTextProps = React.PropsWithChildren +export type ItemIconProps = React.PropsWithChildren<{ + icon: React.ComponentType + position?: 'left' | 'right' +}> + +export type GroupProps = React.PropsWithChildren diff --git a/src/view/screens/Storybook/Menus.tsx b/src/view/screens/Storybook/Menus.tsx new file mode 100644 index 0000000000..082fb2b6e1 --- /dev/null +++ b/src/view/screens/Storybook/Menus.tsx @@ -0,0 +1,79 @@ +import React from 'react' +import {View} from 'react-native' + +import {atoms as a, useTheme} from '#/alf' +import {Text} from '#/components/Typography' +import * as Menu from '#/components/Menu' +import {MagnifyingGlass2_Stroke2_Corner0_Rounded as Search} from '#/components/icons/MagnifyingGlass2' +// import {useDialogStateControlContext} from '#/state/dialogs' + +export function Menus() { + const t = useTheme() + const menuControl = Menu.useMenuControl() + // const {closeAllDialogs} = useDialogStateControlContext() + + return ( + + + + + {({state, props}) => { + return ( + + Open + + ) + }} + + + + + {}}> + + Click me + + + menuControl.close()}> + Another item + + + + + + + {}}> + + Click me + + + menuControl.close()}> + Another item + + + + + + {}}> + + Click me + + + + + + ) +} diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx index 40929555e5..e43d756de5 100644 --- a/src/view/screens/Storybook/index.tsx +++ b/src/view/screens/Storybook/index.tsx @@ -16,6 +16,7 @@ import {Dialogs} from './Dialogs' import {Breakpoints} from './Breakpoints' import {Shadows} from './Shadows' import {Icons} from './Icons' +import {Menus} from './Menus' export function Storybook() { const t = useTheme() @@ -84,6 +85,7 @@ export function Storybook() { + diff --git a/web/index.html b/web/index.html index 8f2275a7f4..b6e01ba4c2 100644 --- a/web/index.html +++ b/web/index.html @@ -217,6 +217,7 @@ } /* NativeDropdown component */ + .radix-dropdown-item:focus, .nativeDropdown-item:focus { outline: none; } diff --git a/yarn.lock b/yarn.lock index ceb712ce27..c3ddb8ee04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4467,6 +4467,18 @@ "@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-escape-keydown" "1.0.3" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + "@radix-ui/react-dropdown-menu@^2.0.1": version "2.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170" @@ -4481,6 +4493,20 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-dropdown-menu@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63" + integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-menu" "2.0.6" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-focus-guards@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" @@ -4498,6 +4524,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -4531,6 +4567,31 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" + integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-popper@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" @@ -4548,6 +4609,23 @@ "@radix-ui/react-use-size" "1.0.1" "@radix-ui/rect" "1.0.1" +"@radix-ui/react-popper@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" + integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + "@radix-ui/react-portal@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" @@ -4556,6 +4634,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-primitive" "1.0.3" +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -18372,6 +18458,13 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-keyed-flatten-children@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/react-keyed-flatten-children/-/react-keyed-flatten-children-3.0.0.tgz#b6ad0bde437d3ab86c8af3a1902d164be2a29d67" + integrity sha512-tSH6gvOyQjt3qtjG+kU9sTypclL1672yjpVufcE3aHNM0FhvjBUQZqsb/awIux4zEuVC3k/DP4p0GdTT/QUt/Q== + dependencies: + react-is "^18.2.0" + react-native-appstate-hook@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06" From 26fc0cf66d0cde33e8105495785a1ce4248fb9f7 Mon Sep 17 00:00:00 2001 From: Eiichi Yoshikawa Date: Wed, 6 Mar 2024 14:28:27 +0900 Subject: [PATCH 03/29] Improve splash display on android (#3105) * Set window background during startup on android * Set statusbar color to transparent and fine-tune it --- app.config.js | 20 +++++++++++++++---- ...withAndroidStylesWindowBackgroundPlugin.js | 20 +++++++++++++++++++ src/App.native.tsx | 3 +++ 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 plugins/withAndroidStylesWindowBackgroundPlugin.js diff --git a/app.config.js b/app.config.js index 9691cab234..530e07b9b3 100644 --- a/app.config.js +++ b/app.config.js @@ -11,6 +11,17 @@ const DARK_SPLASH_CONFIG = { resizeMode: 'cover', } +const SPLASH_CONFIG_ANDROID = { + backgroundColor: '#0c7cff', + image: './assets/splash.png', + resizeMode: 'cover', +} +const DARK_SPLASH_CONFIG_ANDROID = { + backgroundColor: '#0f141b', + image: './assets/splash-dark.png', + resizeMode: 'cover', +} + module.exports = function (config) { /** * App version number. Should be incremented as part of a release cycle. @@ -70,8 +81,8 @@ module.exports = function (config) { }, }, androidStatusBar: { - barStyle: 'dark-content', - backgroundColor: '#ffffff', + barStyle: 'light-content', + backgroundColor: '#00000000', }, android: { icon: './assets/icon.png', @@ -101,8 +112,8 @@ module.exports = function (config) { }, ], splash: { - ...SPLASH_CONFIG, - dark: DARK_SPLASH_CONFIG, + ...SPLASH_CONFIG_ANDROID, + dark: DARK_SPLASH_CONFIG_ANDROID, }, }, web: { @@ -146,6 +157,7 @@ module.exports = function (config) { }, ], './plugins/withAndroidManifestPlugin.js', + './plugins/withAndroidStylesWindowBackgroundPlugin.js', './plugins/shareExtension/withShareExtensions.js', ].filter(Boolean), extra: { diff --git a/plugins/withAndroidStylesWindowBackgroundPlugin.js b/plugins/withAndroidStylesWindowBackgroundPlugin.js new file mode 100644 index 0000000000..427f43df07 --- /dev/null +++ b/plugins/withAndroidStylesWindowBackgroundPlugin.js @@ -0,0 +1,20 @@ +const {withAndroidStyles, AndroidConfig} = require('@expo/config-plugins') + +module.exports = function withAndroidStylesWindowBackgroundPlugin(appConfig) { + return withAndroidStyles(appConfig, function (decoratedAppConfig) { + try { + decoratedAppConfig.modResults = AndroidConfig.Styles.assignStylesValue( + decoratedAppConfig.modResults, + { + add: true, + parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(), + name: 'android:windowBackground', + value: '@drawable/splashscreen', + }, + ) + } catch (e) { + console.error(`withAndroidStylesWindowBackgroundPlugin failed`, e) + } + return decoratedAppConfig + }) +} diff --git a/src/App.native.tsx b/src/App.native.tsx index f08a6235bb..4da3f85f06 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -46,6 +46,8 @@ import {Provider as PortalProvider} from '#/components/Portal' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useIntentHandler} from 'lib/hooks/useIntentHandler' +import {StatusBar} from 'expo-status-bar' +import {isAndroid} from 'platform/detection' SplashScreen.preventAutoHideAsync() @@ -69,6 +71,7 @@ function InnerApp() { return ( + {isAndroid && } Date: Wed, 6 Mar 2024 05:55:34 +0000 Subject: [PATCH 04/29] Initial feature gating and A/B testing integration (#3122) * Add statsig dependency * Add SDK provider * Move to separate file, add tier and hashing * Disable local storage for now * Add initial gate testing fixture * Fork for web just in case * More WIP * wip * Rm test gate * Add shim on native * Clarify --- package.json | 2 + src/App.native.tsx | 33 ++++--- src/App.web.tsx | 33 ++++--- src/lib/statsig/statsig.tsx | 11 +++ src/lib/statsig/statsig.web.tsx | 51 ++++++++++ yarn.lock | 162 +++++++++++++++++++++++++++++++- 6 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 src/lib/statsig/statsig.tsx create mode 100644 src/lib/statsig/statsig.web.tsx diff --git a/package.json b/package.json index cd8cc76519..59ee331939 100644 --- a/package.json +++ b/package.json @@ -179,6 +179,8 @@ "react-responsive": "^9.0.2", "rn-fetch-blob": "^0.12.0", "sentry-expo": "~7.0.1", + "statsig-react": "^1.36.0", + "statsig-react-native-expo": "^4.6.1", "tippy.js": "^6.3.7", "tlds": "^1.234.0", "use-deep-compare": "^1.1.0", diff --git a/src/App.native.tsx b/src/App.native.tsx index 4da3f85f06..eff8ab0997 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -43,6 +43,7 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre import * as persisted from '#/state/persisted' import {Splash} from '#/Splash' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useIntentHandler} from 'lib/hooks/useIntentHandler' @@ -77,21 +78,23 @@ function InnerApp() { - - - - - {/* All components should be within this provider */} - - - - - - - - - - + + + + + + {/* All components should be within this provider */} + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 6ac32a0116..eb2e425930 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -32,6 +32,7 @@ import { import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import * as persisted from '#/state/persisted' import {Provider as PortalProvider} from '#/components/Portal' +import {Provider as StatsigProvider} from '#/lib/statsig/statsig' import {useIntentHandler} from 'lib/hooks/useIntentHandler' function InnerApp() { @@ -54,21 +55,23 @@ function InnerApp() { - - - - - {/* All components should be within this provider */} - - - - - - - - - - + + + + + + {/* All components should be within this provider */} + + + + + + + + + + + ) diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx new file mode 100644 index 0000000000..88a57c3fcd --- /dev/null +++ b/src/lib/statsig/statsig.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +export function useGate(_gateName: string) { + // Not enabled for native yet. + return false +} + +export function Provider({children}: {children: React.ReactNode}) { + // Not enabled for native yet. + return children +} diff --git a/src/lib/statsig/statsig.web.tsx b/src/lib/statsig/statsig.web.tsx new file mode 100644 index 0000000000..6508131c44 --- /dev/null +++ b/src/lib/statsig/statsig.web.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import {StatsigProvider, useGate as useStatsigGate} from 'statsig-react' +import {useSession} from '../../state/session' +import {sha256} from 'js-sha256' + +const statsigOptions = { + environment: { + tier: process.env.NODE_ENV === 'development' ? 'development' : 'production', + }, + // Don't block on waiting for network. The fetched config will kick in on next load. + // This ensures the UI is always consistent and doesn't update mid-session. + // Note this makes cold load (no local storage) and private mode return `false` for all gates. + initTimeoutMs: 1, +} + +export function useGate(gateName: string) { + const {isLoading, value} = useStatsigGate(gateName) + if (isLoading) { + // This should not happen because of waitForInitialization={true}. + console.error('Did not expected isLoading to ever be true.') + } + return value +} + +function toStatsigUser(did: string | undefined) { + let userID: string | undefined + if (did) { + userID = sha256(did) + } + return {userID} +} + +export function Provider({children}: {children: React.ReactNode}) { + const {currentAccount} = useSession() + const currentStatsigUser = React.useMemo( + () => toStatsigUser(currentAccount?.did), + [currentAccount?.did], + ) + return ( + + {children} + + ) +} diff --git a/yarn.lock b/yarn.lock index c3ddb8ee04..3add1af644 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3115,6 +3115,27 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@~5.0.3": + version "5.0.4" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-5.0.4.tgz#216fea6558fe66615af1370de55193f4181cb23e" + integrity sha512-vzUcVpqOMs3h+hyRdhGwk+eGIOhXa5xYdd92yO17RMNHav3v/+ekMbs7XA2c3lepMO8Yd4/5hqmRw9ZTL6jGzg== + dependencies: + "@expo/config-types" "^47.0.0" + "@expo/json-file" "8.2.36" + "@expo/plist" "0.0.18" + "@expo/sdk-runtime-versions" "^1.0.0" + "@react-native/normalize-color" "^2.0.0" + chalk "^4.1.2" + debug "^4.3.1" + find-up "~5.0.0" + getenv "^1.0.0" + glob "7.1.6" + resolve-from "^5.0.0" + semver "^7.3.5" + slash "^3.0.0" + xcode "^3.0.1" + xml2js "0.4.23" + "@expo/config-plugins@~7.8.2": version "7.8.2" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.2.tgz#c00ce93c4d6c2cb9e345ed9cd56ceeea05ab8ddb" @@ -3138,6 +3159,11 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-types@^47.0.0": + version "47.0.0" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d" + integrity sha512-r0pWfuhkv7KIcXMUiNACJmJKKwlTBGMw9VZHNdppS8/0Nve8HZMTkNRFQzTHW1uH3pBj8jEXpyw/2vSWDHex9g== + "@expo/config-types@^50.0.0", "@expo/config-types@^50.0.0-alpha.1": version "50.0.0" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b" @@ -3160,6 +3186,23 @@ slugify "^1.3.4" sucrase "^3.20.0" +"@expo/config@~7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-7.0.3.tgz#c9c634e76186de25e296485e51418f1e52966e6e" + integrity sha512-joVtB5o+NF40Tmsdp65UzryRtbnCuMbXkVO4wJnNJO4aaK0EYLdHCYSewORVqNcDfGN0LphQr8VTG2npbd9CJA== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~5.0.3" + "@expo/config-types" "^47.0.0" + "@expo/json-file" "8.2.36" + getenv "^1.0.0" + glob "7.1.6" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + semver "7.3.2" + slugify "^1.3.4" + sucrase "^3.20.0" + "@expo/config@~8.5.0": version "8.5.0" resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.0.tgz#c618e016c3272335e33fec02fb7fd67e4dcc3342" @@ -3259,6 +3302,15 @@ semver "7.3.2" tempy "0.3.0" +"@expo/json-file@8.2.36": + version "8.2.36" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.36.tgz#62a505cb7f30a34d097386476794680a3f7385ff" + integrity sha512-tOZfTiIFA5KmMpdW9KF7bc6CFiGjb0xnbieJhTGlHrLL+ps2G0OkqmuZ3pFEXBOMnJYUVpnSy++52LFxvpa5ZQ== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^1.0.1" + write-file-atomic "^2.3.0" + "@expo/json-file@^8.2.37": version "8.2.37" resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.37.tgz#9c02d3b42134907c69cc0a027b18671b69344049" @@ -3328,6 +3380,15 @@ split "^1.0.1" sudo-prompt "9.1.1" +"@expo/plist@0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.18.tgz#9abcde78df703a88f6d9fa1a557ee2f045d178b0" + integrity sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w== + dependencies: + "@xmldom/xmldom" "~0.7.0" + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + "@expo/plist@^0.1.0": version "0.1.0" resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.0.tgz#eabc95f951d14e10c87fd0443ee01d567371f058" @@ -4750,6 +4811,13 @@ dependencies: merge-options "^3.0.4" +"@react-native-async-storage/async-storage@^1.15.2": + version "1.22.0" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc" + integrity sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA== + dependencies: + merge-options "^3.0.4" + "@react-native-camera-roll/camera-roll@^5.2.2": version "5.7.2" resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz#db11525ae26c8a61630c424aebd323a7c784a921" @@ -8124,7 +8192,7 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== -"@xmldom/xmldom@~0.7.7": +"@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7": version "0.7.13" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3" integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g== @@ -11737,6 +11805,14 @@ expo-camera@~14.0.1: dependencies: invariant "^2.2.4" +expo-constants@^13.0.2: + version "13.2.4" + resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-13.2.4.tgz#eab4a553f074b2c60ad7a158d3b82e3484a94606" + integrity sha512-Zobau8EuTk2GgafwkfGnWM6CmSLB7X8qnQXVuXe0nd3v92hfQUmRWGhJwH88uxXj3LrfqctM6PaJ8taG1vxfBw== + dependencies: + "@expo/config" "~7.0.0" + uuid "^3.3.2" + expo-constants@~15.4.0: version "15.4.1" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.1.tgz#f76f347cf687b6630e1e3b9a385a4e42771671a4" @@ -11786,6 +11862,13 @@ expo-dev-menu@4.5.3: expo-dev-menu-interface "1.7.2" semver "^7.5.3" +expo-device@~4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-4.1.1.tgz#5de94144113ffb7fa0f37fa3d70e452113954c10" + integrity sha512-It0SGtKcvzQSf+Co6zdPdB63zZvG2/rDolB1lqswMNKj03Y7KVU41s5tcQCqNczj7tmeN3CJy7A8YhYGKdb7gA== + dependencies: + ua-parser-js "^0.7.19" + expo-device@~5.9.2: version "5.9.2" resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.9.2.tgz#697e96f52d213a141b6f265f1e274e9d5e98c92c" @@ -14978,6 +15061,16 @@ js-queue@2.0.2: dependencies: easy-stack "^1.0.1" +js-sha256@^0.10.1: + version "0.10.1" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d" + integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw== + +js-sha256@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576" + integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q== + js-sha256@^0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" @@ -15162,7 +15255,7 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^1.0.2: +json5@^1.0.1, json5@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== @@ -18503,6 +18596,13 @@ react-native-gesture-handler@~2.14.0: lodash "^4.17.21" prop-types "^15.7.2" +react-native-get-random-values@^1.6.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.10.0.tgz#c2c5f12a4ef8b1175145347b4a4b9f9a40d9ffc8" + integrity sha512-gZ1zbXhbb8+Jy9qYTV8c4Nf45/VB4g1jmXuavY5rPfUn7x3ok9Vl3FTl0dnE92Z4FFtfbUNNwtSfcmomdtWg+A== + dependencies: + fast-base64-decode "^1.0.0" + react-native-get-random-values@~1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16" @@ -19976,6 +20076,49 @@ standard-as-callback@^2.1.0: resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== +statsig-js@4.45.1: + version "4.45.1" + resolved "https://registry.yarnpkg.com/statsig-js/-/statsig-js-4.45.1.tgz#b1f5b9c52adc4a8aece376fb011416c89227932f" + integrity sha512-h94RzFQsJCQCNwQXpZ9OBXcvCxDnkXF6OrCekd81ySvY2l4JSowpxMWX3Iw6IDFzfTfKdER9JQzFLhMSQbT+YQ== + dependencies: + js-sha256 "^0.10.1" + uuid "^8.3.2" + +statsig-js@4.49.0: + version "4.49.0" + resolved "https://registry.yarnpkg.com/statsig-js/-/statsig-js-4.49.0.tgz#8470a9ac218a93d36f4b7b306ff9377e48064740" + integrity sha512-N4drx6fzI168Q4NndFY3IJbSDqpWSBWvS290H/RnT/g3Et58SvtXzG5qqgzmqy4CwcmwH+IL8K15pL7hPnfvUQ== + dependencies: + js-sha256 "^0.11.0" + uuid "^8.3.2" + +statsig-react-native-expo@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/statsig-react-native-expo/-/statsig-react-native-expo-4.6.1.tgz#0bdf49fee7112f7f28bff2405f4ba0c1727bb3d6" + integrity sha512-rB60c+WSrQPmjW9j75d+acUtwSOe38PE2KTDHiOv1Mf+0TCcFtGYlJmKCibWvbeXR7ZAyjjGeroh23bCSEZauQ== + dependencies: + "@react-native-async-storage/async-storage" "^1.15.2" + expo-constants "^13.0.2" + expo-device "~4.1.1" + js-sha256 "^0.9.0" + react-native-get-random-values "^1.6.0" + statsig-react "^1.21.1" + uuid "^8.3.2" + +statsig-react@^1.21.1: + version "1.35.0" + resolved "https://registry.yarnpkg.com/statsig-react/-/statsig-react-1.35.0.tgz#ad5730b83f564c640623e954fcbcbe848e939946" + integrity sha512-KLN7dhq6FvAl25Z0QN6IINFBgM3yn0GMafoE698tYZqRf911xvevFaR7qUXiTz3W9vmFYrmFRouqVMfCv7DW0A== + dependencies: + statsig-js "4.45.1" + +statsig-react@^1.36.0: + version "1.36.0" + resolved "https://registry.yarnpkg.com/statsig-react/-/statsig-react-1.36.0.tgz#c2171268a6c76eee534849ec9556b836baba04b6" + integrity sha512-QcTHla3ypfn2RvrnHGNlqWbiC2W/ZjcMM5LT6ExNV4skH7Xhspto3dMS3JVzBhOb74OEDZK4DbxQj9Wdz6XW0w== + dependencies: + statsig-js "4.49.0" + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" @@ -20920,6 +21063,11 @@ typescript@^5.3.3: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +ua-parser-js@^0.7.19: + version "0.7.37" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832" + integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA== + ua-parser-js@^0.7.33: version "0.7.35" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" @@ -21201,7 +21349,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^3.0.1: +uuid@^3.0.1, uuid@^3.3.2: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -21953,6 +22101,14 @@ xml-name-validator@^4.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml2js@0.4.23: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + xml2js@0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282" From 2e0f8064411cc8d06013276353ce72ac8d9a0e2b Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 6 Mar 2024 16:12:37 +0000 Subject: [PATCH 05/29] Fix jumpy moderation icon on desktop (#3125) --- src/view/shell/desktop/LeftNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx index def0333c7c..c56ba941ed 100644 --- a/src/view/shell/desktop/LeftNav.tsx +++ b/src/view/shell/desktop/LeftNav.tsx @@ -391,7 +391,7 @@ export function DesktopLeftNav() { } label={_(msg`Moderation`)} From 5b8d116e335bd7574928485adc1d4e9cf5ee0564 Mon Sep 17 00:00:00 2001 From: dan Date: Wed, 6 Mar 2024 17:27:59 +0000 Subject: [PATCH 06/29] By default, hide replies to people you don't follow in the Following feed (#3124) * Show replies from followed by default * Update @atproto/api --- package.json | 2 +- src/state/queries/preferences/const.ts | 2 +- yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 59ee331939..c9909dcfe5 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "update-extensions": "scripts/updateExtensions.sh" }, "dependencies": { - "@atproto/api": "^0.10.4", + "@atproto/api": "^0.10.5", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@emoji-mart/react": "^1.1.1", diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts index 25d2849987..53c9e482af 100644 --- a/src/state/queries/preferences/const.ts +++ b/src/state/queries/preferences/const.ts @@ -7,7 +7,7 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/ export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] = { hideReplies: false, - hideRepliesByUnfollowed: false, + hideRepliesByUnfollowed: true, hideRepliesByLikeCount: 0, hideReposts: false, hideQuotePosts: false, diff --git a/yarn.lock b/yarn.lock index 3add1af644..9d73b0688e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34,10 +34,10 @@ jsonpointer "^5.0.0" leven "^3.1.0" -"@atproto/api@^0.10.4": - version "0.10.4" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.4.tgz#b73446f2344783c42c6040082756449443f15750" - integrity sha512-9gwZt4v4pngfD4mgsET9i9Ym0PpMSzftTzqBjCbFpObx15zMkFemYnLUnyT/NEww2u/aRxjAe2TeBnU0dIbbuQ== +"@atproto/api@^0.10.5": + version "0.10.5" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.5.tgz#e778e2843d08690df8df81f24028a7578e9b3cb4" + integrity sha512-GYdST5sPKU2JnPmm8x3KqjOSlDiYXrp4GkW7bpQTVLPabnUNq5NLN6HJEoJABjjOAsaLF12rBoV+JpRb1UjNsQ== dependencies: "@atproto/common-web" "^0.2.3" "@atproto/lexicon" "^0.3.2" From f61d1e1f9410865505a316d825a314354a84171d Mon Sep 17 00:00:00 2001 From: Eiichi Yoshikawa Date: Thu, 7 Mar 2024 03:24:08 +0900 Subject: [PATCH 07/29] Apply notification icon settings of FCM on Android (#3113) Co-authored-by: Hailey --- app.config.js | 3 +- plugins/withAndroidManifestFCMIconPlugin.js | 37 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 plugins/withAndroidManifestFCMIconPlugin.js diff --git a/app.config.js b/app.config.js index 530e07b9b3..2552d34897 100644 --- a/app.config.js +++ b/app.config.js @@ -153,10 +153,11 @@ module.exports = function (config) { 'expo-notifications', { icon: './assets/icon-android-notification.png', - color: '#ffffff', + color: '#1185fe', }, ], './plugins/withAndroidManifestPlugin.js', + './plugins/withAndroidManifestFCMIconPlugin.js', './plugins/withAndroidStylesWindowBackgroundPlugin.js', './plugins/shareExtension/withShareExtensions.js', ].filter(Boolean), diff --git a/plugins/withAndroidManifestFCMIconPlugin.js b/plugins/withAndroidManifestFCMIconPlugin.js new file mode 100644 index 0000000000..066a975d87 --- /dev/null +++ b/plugins/withAndroidManifestFCMIconPlugin.js @@ -0,0 +1,37 @@ +const {withAndroidManifest} = require('expo/config-plugins') + +module.exports = function withAndroidManifestFCMIconPlugin(appConfig) { + return withAndroidManifest(appConfig, function (decoratedAppConfig) { + try { + function addOrModifyMetaData(metaData, name, resource) { + const elem = metaData.find(elem => elem.$['android:name'] === name) + if (elem === undefined) { + metaData.push({ + $: { + 'android:name': name, + 'android:resource': resource, + }, + }) + } else { + elem.$['android:resource'] = resource + } + } + const androidManifest = decoratedAppConfig.modResults.manifest + const metaData = androidManifest.application[0]['meta-data'] + addOrModifyMetaData( + metaData, + 'com.google.firebase.messaging.default_notification_color', + '@color/notification_icon_color', + ) + addOrModifyMetaData( + metaData, + 'com.google.firebase.messaging.default_notification_icon', + '@drawable/notification_icon', + ) + return decoratedAppConfig + } catch (e) { + console.error(`withAndroidManifestFCMIconPlugin failed`, e) + } + return decoratedAppConfig + }) +} From 7a592d8140cbb907535b482cb43a369b0a7e4750 Mon Sep 17 00:00:00 2001 From: Jake Gold <52801504+Jacob2161@users.noreply.github.com> Date: Wed, 6 Mar 2024 10:33:41 -0800 Subject: [PATCH 08/29] bskyweb: remove waitlist email endpoint (#3127) --- bskyweb/cmd/bskyweb/mailmodo.go | 70 --------------------------------- bskyweb/cmd/bskyweb/main.go | 12 ------ bskyweb/cmd/bskyweb/server.go | 55 +++----------------------- 3 files changed, 5 insertions(+), 132 deletions(-) delete mode 100644 bskyweb/cmd/bskyweb/mailmodo.go diff --git a/bskyweb/cmd/bskyweb/mailmodo.go b/bskyweb/cmd/bskyweb/mailmodo.go deleted file mode 100644 index e892971f9c..0000000000 --- a/bskyweb/cmd/bskyweb/mailmodo.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/json" - "fmt" - "net/http" - "time" -) - -type Mailmodo struct { - httpClient *http.Client - APIKey string - BaseURL string - ListName string -} - -func NewMailmodo(apiKey, listName string) *Mailmodo { - return &Mailmodo{ - APIKey: apiKey, - BaseURL: "https://api.mailmodo.com/api/v1", - httpClient: &http.Client{}, - ListName: listName, - } -} - -func (m *Mailmodo) request(ctx context.Context, httpMethod string, apiMethod string, data any) error { - endpoint := fmt.Sprintf("%s/%s", m.BaseURL, apiMethod) - js, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("Mailmodo JSON encoding failed: %w", err) - } - req, err := http.NewRequestWithContext(ctx, httpMethod, endpoint, bytes.NewBuffer(js)) - if err != nil { - return fmt.Errorf("Mailmodo HTTP creating request %s %s failed: %w", httpMethod, apiMethod, err) - } - req.Header.Set("mmApiKey", m.APIKey) - req.Header.Set("Content-Type", "application/json") - - res, err := m.httpClient.Do(req) - if err != nil { - return fmt.Errorf("Mailmodo HTTP making request %s %s failed: %w", httpMethod, apiMethod, err) - } - defer res.Body.Close() - - status := struct { - Success bool `json:"success"` - Message string `json:"message"` - }{} - if err := json.NewDecoder(res.Body).Decode(&status); err != nil { - return fmt.Errorf("Mailmodo HTTP parsing response %s %s failed: %w", httpMethod, apiMethod, err) - } - if !status.Success { - return fmt.Errorf("Mailmodo API response %s %s failed: %s", httpMethod, apiMethod, status.Message) - } - return nil -} - -func (m *Mailmodo) AddToList(ctx context.Context, email string) error { - return m.request(ctx, "POST", "addToList", map[string]any{ - "listName": m.ListName, - "email": email, - "data": map[string]any{ - "email_hashed": fmt.Sprintf("%x", sha256.Sum256([]byte(email))), - }, - "created_at": time.Now().UTC().Format(time.RFC3339), - }) -} diff --git a/bskyweb/cmd/bskyweb/main.go b/bskyweb/cmd/bskyweb/main.go index a2952cae2b..5185ff573a 100644 --- a/bskyweb/cmd/bskyweb/main.go +++ b/bskyweb/cmd/bskyweb/main.go @@ -40,18 +40,6 @@ func run(args []string) { // retain old PDS env var for easy transition EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, }, - &cli.StringFlag{ - Name: "mailmodo-api-key", - Usage: "Mailmodo API key", - Required: false, - EnvVars: []string{"MAILMODO_API_KEY"}, - }, - &cli.StringFlag{ - Name: "mailmodo-list-name", - Usage: "Mailmodo contact list to add email addresses to", - Required: false, - EnvVars: []string{"MAILMODO_LIST_NAME"}, - }, &cli.StringFlag{ Name: "http-address", Usage: "Specify the local IP/port to bind to", diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 6b76acc948..e159d780a2 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -2,11 +2,9 @@ package main import ( "context" - "encoding/json" "errors" "fmt" "io/fs" - "io/ioutil" "net/http" "os" "os/signal" @@ -29,25 +27,19 @@ import ( ) type Server struct { - echo *echo.Echo - httpd *http.Server - mailmodo *Mailmodo - xrpcc *xrpc.Client + echo *echo.Echo + httpd *http.Server + xrpcc *xrpc.Client } func serve(cctx *cli.Context) error { debug := cctx.Bool("debug") httpAddress := cctx.String("http-address") appviewHost := cctx.String("appview-host") - mailmodoAPIKey := cctx.String("mailmodo-api-key") - mailmodoListName := cctx.String("mailmodo-list-name") // Echo e := echo.New() - // Mailmodo client. - mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName) - // create a new session (no auth) xrpcc := &xrpc.Client{ Client: cliutil.NewHttpClient(), @@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error { // server // server := &Server{ - echo: e, - mailmodo: mailmodo, - xrpcc: xrpcc, + echo: e, + xrpcc: xrpcc, } // Create the HTTP server. @@ -221,9 +212,6 @@ func serve(cctx *cli.Context) error { e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) - // Mailmodo - e.POST("/api/waitlist", server.apiWaitlist) - // Start the server. log.Infof("starting server address=%s", httpAddress) go func() { @@ -398,36 +386,3 @@ func (srv *Server) WebProfile(c echo.Context) error { data["requestHost"] = req.Host return c.Render(http.StatusOK, "profile.html", data) } - -func (srv *Server) apiWaitlist(c echo.Context) error { - type jsonError struct { - Error string `json:"error"` - } - - // Read the API request. - type apiRequest struct { - Email string `json:"email"` - } - - bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024) - payload, err := ioutil.ReadAll(bodyReader) - if err != nil { - return err - } - var req apiRequest - if err := json.Unmarshal(payload, &req); err != nil { - return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"}) - } - - if req.Email == "" { - return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."}) - } - - if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil { - log.Errorf("adding email to waitlist failed: %s", err) - return c.JSON(http.StatusBadRequest, jsonError{ - Error: "Storing email in waitlist failed. Please enter a valid email address.", - }) - } - return c.JSON(http.StatusOK, map[string]bool{"success": true}) -} From b8f36a8bca0eab08e09e0a56be63ade40e710f34 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 11:02:48 -0800 Subject: [PATCH 09/29] Fix double border on web hashtag list (#3091) * add `topBorder` to Views.tsx * fix double border --- src/components/Lists.tsx | 13 ++++++++----- src/screens/Hashtag.tsx | 7 ++----- src/view/com/util/Views.d.ts | 4 +++- src/view/com/util/Views.web.tsx | 11 ++++++++++- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/Lists.tsx b/src/components/Lists.tsx index 12a935807e..58aa74b388 100644 --- a/src/components/Lists.tsx +++ b/src/components/Lists.tsx @@ -1,6 +1,7 @@ import React from 'react' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {View} from 'react-native' +import {CenteredView} from 'view/com/util/Views' import {Loader} from '#/components/Loader' import {Trans} from '@lingui/macro' import {cleanError} from 'lib/strings/errors' @@ -143,7 +144,7 @@ export function ListMaybePlaceholder({ }) { const navigation = useNavigation() const t = useTheme() - const {gtMobile} = useBreakpoints() + const {gtMobile, gtTablet} = useBreakpoints() const canGoBack = navigation.canGoBack() const onGoBack = React.useCallback(() => { @@ -165,14 +166,16 @@ export function ListMaybePlaceholder({ if (!isEmpty) return null return ( - + ]} + sideBorders={gtMobile} + topBorder={!gtTablet}> {isLoading ? ( @@ -241,6 +244,6 @@ export function ListMaybePlaceholder({ )} - + ) } diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 09a1f28247..82ea75aa2c 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -1,6 +1,5 @@ import React from 'react' import {ListRenderItemInfo, Pressable} from 'react-native' -import {atoms as a, useBreakpoints} from '#/alf' import {useFocusEffect} from '@react-navigation/native' import {useSetMinimalShellMode} from 'state/shell' import {ViewHeader} from 'view/com/util/ViewHeader' @@ -19,7 +18,6 @@ import {List} from 'view/com/util/List' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {sanitizeHandle} from 'lib/strings/handles' -import {CenteredView} from 'view/com/util/Views' import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' import {shareUrl} from 'lib/sharing' import {HITSLOP_10} from 'lib/constants' @@ -38,7 +36,6 @@ export default function HashtagScreen({ }: NativeStackScreenProps) { const {tag, author} = route.params const setMinimalShellMode = useSetMinimalShellMode() - const {gtMobile} = useBreakpoints() const {_} = useLingui() const [isPTR, setIsPTR] = React.useState(false) @@ -103,7 +100,7 @@ export default function HashtagScreen({ }, [isFetching, hasNextPage, error, fetchNextPage]) return ( - + <> )} - + ) } diff --git a/src/view/com/util/Views.d.ts b/src/view/com/util/Views.d.ts index 6a90cc229b..16713921fb 100644 --- a/src/view/com/util/Views.d.ts +++ b/src/view/com/util/Views.d.ts @@ -5,4 +5,6 @@ export function CenteredView({ style, sideBorders, ...props -}: React.PropsWithChildren) +}: React.PropsWithChildren< + ViewProps & {sideBorders?: boolean; topBorder?: boolean} +>) diff --git a/src/view/com/util/Views.web.tsx b/src/view/com/util/Views.web.tsx index db3b9de0d8..ae165077ca 100644 --- a/src/view/com/util/Views.web.tsx +++ b/src/view/com/util/Views.web.tsx @@ -32,8 +32,11 @@ interface AddedProps { export function CenteredView({ style, sideBorders, + topBorder, ...props -}: React.PropsWithChildren) { +}: React.PropsWithChildren< + ViewProps & {sideBorders?: boolean; topBorder?: boolean} +>) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() if (!isMobile) { @@ -46,6 +49,12 @@ export function CenteredView({ }) style = addStyle(style, pal.border) } + if (topBorder) { + style = addStyle(style, { + borderTopWidth: 1, + }) + style = addStyle(style, pal.border) + } return } From 2d9a5db967c7eb98228ae372c1139e4069974b90 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 11:13:03 -0800 Subject: [PATCH 10/29] Switch date picker libraries (#3126) * Revert other base PR changes, switch date picker library rm expo-linear-gradient Revert "remove unused packages, switch to `expo-linear-gradient`" This reverts commit 20a0ffcf Revert "upgrade expo deps" This reverts commit a43dca92caa120d45fb771431752dd2db3b37ab5. Revert other library changes This reverts commit 5219f66e Revert "re-add normalize-url" This reverts commit 654019c4babe2e455f6069a6c725eb51140ab1ab. Revert "add `expo-haptics`" This reverts commit ff3a0399b1c7eae07b83946f13543eedf7cdfe82. Revert "add `expo-clipboard`" This reverts commit 440ae91632153e22ff79050d8ac803a7305e88a0. Revert "add `expo-file-system`" This reverts commit c0fe0c94534564982399c22299a8a19052bf3e54. fix android alf use modal on android migrate to `react-native-date-picker` rm `@reactnativecommunity/datetimepicker` add `react-native-date-picker` add `expo-file-system` add `expo-clipboard` add `expo-haptics` re-add normalize-url rm blur view upgrade expo deps remove unused packages, switch to `expo-linear-gradient` * rm old library * Use UTC for dates --------- Co-authored-by: Eric Bailey --- package.json | 2 +- .../forms/DateField/index.android.tsx | 39 +++++++++---------- src/components/forms/DateField/index.tsx | 20 ++++------ src/state/queries/preferences/index.ts | 2 +- src/view/com/util/forms/DateInput.tsx | 32 ++++++++------- yarn.lock | 12 +++--- 6 files changed, 51 insertions(+), 56 deletions(-) diff --git a/package.json b/package.json index c9909dcfe5..294b138c47 100644 --- a/package.json +++ b/package.json @@ -63,7 +63,6 @@ "@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-clipboard/clipboard": "^1.10.0", "@react-native-community/blur": "^4.3.0", - "@react-native-community/datetimepicker": "7.6.1", "@react-native-masked-view/masked-view": "0.3.0", "@react-native-menu/menu": "^0.8.0", "@react-native-picker/picker": "2.6.1", @@ -152,6 +151,7 @@ "react-keyed-flatten-children": "^3.0.0", "react-native": "0.73.2", "react-native-appstate-hook": "^1.0.6", + "react-native-date-picker": "^4.4.0", "react-native-drawer-layout": "^4.0.0-alpha.3", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "~2.14.0", diff --git a/src/components/forms/DateField/index.android.tsx b/src/components/forms/DateField/index.android.tsx index cddb643d61..451810a5ea 100644 --- a/src/components/forms/DateField/index.android.tsx +++ b/src/components/forms/DateField/index.android.tsx @@ -1,8 +1,5 @@ import React from 'react' import {View, Pressable} from 'react-native' -import DateTimePicker, { - BaseProps as DateTimePickerProps, -} from '@react-native-community/datetimepicker' import {useTheme, atoms} from '#/alf' import {Text} from '#/components/Typography' @@ -15,6 +12,8 @@ import { localizeDate, toSimpleDateString, } from '#/components/forms/DateField/utils' +import DatePicker from 'react-native-date-picker' +import {isAndroid} from 'platform/detection' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -38,20 +37,20 @@ export function DateField({ const {chromeFocus, chromeError, chromeErrorHover} = TextField.useSharedInputStyles() - const onChangeInternal = React.useCallback< - Required['onChange'] - >( - (_event, date) => { + const onChangeInternal = React.useCallback( + (date: Date) => { setOpen(false) - if (date) { - const formatted = toSimpleDateString(date) - onChangeDate(formatted) - } + const formatted = toSimpleDateString(date) + onChangeDate(formatted) }, [onChangeDate, setOpen], ) + const onCancel = React.useCallback(() => { + setOpen(false) + }, []) + return ( {open && ( - )} diff --git a/src/components/forms/DateField/index.tsx b/src/components/forms/DateField/index.tsx index e65936e0ea..49e47a01e3 100644 --- a/src/components/forms/DateField/index.tsx +++ b/src/components/forms/DateField/index.tsx @@ -1,13 +1,11 @@ import React from 'react' import {View} from 'react-native' -import DateTimePicker, { - DateTimePickerEvent, -} from '@react-native-community/datetimepicker' import {useTheme, atoms} from '#/alf' import * as TextField from '#/components/forms/TextField' import {toSimpleDateString} from '#/components/forms/DateField/utils' import {DateFieldProps} from '#/components/forms/DateField/types' +import DatePicker from 'react-native-date-picker' export * as utils from '#/components/forms/DateField/utils' export const Label = TextField.Label @@ -28,7 +26,7 @@ export function DateField({ const t = useTheme() const onChangeInternal = React.useCallback( - (event: DateTimePickerEvent, date: Date | undefined) => { + (date: Date | undefined) => { if (date) { const formatted = toSimpleDateString(date) onChangeDate(formatted) @@ -39,17 +37,15 @@ export function DateField({ return ( - ) diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 07198de77f..37ef10ae0c 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -169,7 +169,7 @@ export function usePreferencesSetBirthDateMutation() { return useMutation({ mutationFn: async ({birthDate}: {birthDate: Date}) => { - await getAgent().setPersonalDetails({birthDate}) + await getAgent().setPersonalDetails({birthDate: birthDate.toISOString()}) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx index c5f0afc8fc..0104562aa5 100644 --- a/src/view/com/util/forms/DateInput.tsx +++ b/src/view/com/util/forms/DateInput.tsx @@ -1,8 +1,5 @@ import React, {useState, useCallback} from 'react' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' -import DateTimePicker, { - DateTimePickerEvent, -} from '@react-native-community/datetimepicker' import { FontAwesomeIcon, FontAwesomeIconStyle, @@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext' import {usePalette} from 'lib/hooks/usePalette' import {getLocales} from 'expo-localization' +import DatePicker from 'react-native-date-picker' const LOCALE = getLocales()[0] @@ -43,11 +41,9 @@ export function DateInput(props: Props) { }, [props.handleAsUTC]) const onChangeInternal = useCallback( - (event: DateTimePickerEvent, date: Date | undefined) => { + (date: Date) => { setShow(false) - if (date) { - props.onChange(date) - } + props.onChange(date) }, [setShow, props], ) @@ -56,6 +52,10 @@ export function DateInput(props: Props) { setShow(true) }, [setShow]) + const onCancel = useCallback(() => { + setShow(false) + }, []) + return ( {isAndroid && ( @@ -80,15 +80,17 @@ export function DateInput(props: Props) { )} {(isIOS || show) && ( - Date: Wed, 6 Mar 2024 11:34:48 -0800 Subject: [PATCH 11/29] fix internal links showing up as external (#3128) * fix internal links showing up as external * fix internal links showing up as external --- src/lib/strings/url-helpers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index ba2cdb39bc..ee88225ee7 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -158,7 +158,12 @@ export function linkRequiresWarning(uri: string, label: string) { const host = urip.hostname.toLowerCase() // Hosts that end with bsky.app or bsky.social should be trusted by default. - if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) { + if ( + uri.startsWith('/') || + host.endsWith('bsky.app') || + host.endsWith('bsky.social') || + host.endsWith('blueskyweb.xyz') + ) { // if this is a link to internal content, // warn if it represents itself as a URL to another app return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain) From 800eecbfe4746c887933ecd34c20c862220d07ee Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 11:40:10 -0800 Subject: [PATCH 12/29] fix internal links showing up as external pt2 (#3129) * fix internal links showing up as external pt2 * fix internal links showing up as external pt2 --- src/lib/strings/url-helpers.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index ee88225ee7..7729e4a382 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -148,6 +148,11 @@ export function feedUriToHref(url: string): string { export function linkRequiresWarning(uri: string, label: string) { const labelDomain = labelToDomain(label) + // If the uri started with a / we know it is internal. + if (uri.startsWith('/')) { + return false + } + let urip try { urip = new URL(uri) @@ -156,10 +161,8 @@ export function linkRequiresWarning(uri: string, label: string) { } const host = urip.hostname.toLowerCase() - // Hosts that end with bsky.app or bsky.social should be trusted by default. if ( - uri.startsWith('/') || host.endsWith('bsky.app') || host.endsWith('bsky.social') || host.endsWith('blueskyweb.xyz') From d5b0a33ae3b36463684e53b2b99196826fa9d538 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 12:27:49 -0800 Subject: [PATCH 13/29] Run `extract` and `compile` when building for web (#3132) --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fcd2413cdc..3ad05b6ec6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \ nvm use $NODE_VERSION && \ npm install --global yarn && \ yarn && \ - yarn intl:compile && \ + yarn intl:build && \ yarn build-web # DEBUG From 357b61d0a5573209214589112d5f0ac829fc6293 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 15:24:37 -0800 Subject: [PATCH 14/29] version bump (wait for release) (#3095) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 294b138c47..2571fe772d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bsky.app", - "version": "1.71.0", + "version": "1.72.0", "private": true, "engines": { "node": ">=18" From 8b0e575f6423575c08e9a6748be41c888611d631 Mon Sep 17 00:00:00 2001 From: Hailey Date: Wed, 6 Mar 2024 15:33:23 -0800 Subject: [PATCH 15/29] Adjust FlatList performance in main feeds (#3134) * adjust flatlist perf settings * calculate initial num to render based on screen height * adjust window size * don't react to screen height changes --- src/lib/hooks/useInitialNumToRender.ts | 11 +++++++++++ src/screens/Hashtag.tsx | 4 ++++ src/view/com/posts/Feed.tsx | 4 ++++ 3 files changed, 19 insertions(+) create mode 100644 src/lib/hooks/useInitialNumToRender.ts diff --git a/src/lib/hooks/useInitialNumToRender.ts b/src/lib/hooks/useInitialNumToRender.ts new file mode 100644 index 0000000000..942f0404ab --- /dev/null +++ b/src/lib/hooks/useInitialNumToRender.ts @@ -0,0 +1,11 @@ +import React from 'react' +import {Dimensions} from 'react-native' + +const MIN_POST_HEIGHT = 100 + +export function useInitialNumToRender(minItemHeight: number = MIN_POST_HEIGHT) { + return React.useMemo(() => { + const screenHeight = Dimensions.get('window').height + return Math.ceil(screenHeight / minItemHeight) + 1 + }, [minItemHeight]) +} diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index 82ea75aa2c..f1b8173705 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -22,6 +22,7 @@ import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOut import {shareUrl} from 'lib/sharing' import {HITSLOP_10} from 'lib/constants' import {isNative} from 'platform/detection' +import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' const renderItem = ({item}: ListRenderItemInfo) => { return @@ -37,6 +38,7 @@ export default function HashtagScreen({ const {tag, author} = route.params const setMinimalShellMode = useSetMinimalShellMode() const {_} = useLingui() + const initialNumToRender = useInitialNumToRender() const [isPTR, setIsPTR] = React.useState(false) const fullTag = React.useMemo(() => { @@ -154,6 +156,8 @@ export default function HashtagScreen({ onRetry={fetchNextPage} /> } + initialNumToRender={initialNumToRender} + windowSize={11} /> )} diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 54d8aa2240..cd3e98785d 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -32,6 +32,7 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home' +import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender' const LOADING_ITEM = {_reactKey: '__loading__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} @@ -84,6 +85,7 @@ let Feed = ({ const {_} = useLingui() const queryClient = useQueryClient() const {currentAccount} = useSession() + const initialNumToRender = useInitialNumToRender() const [isPTRing, setIsPTRing] = React.useState(false) const checkForNewRef = React.useRef<(() => void) | null>(null) const lastFetchRef = React.useRef(Date.now()) @@ -327,6 +329,8 @@ let Feed = ({ desktopFixedHeight={ desktopFixedHeightOffset ? desktopFixedHeightOffset : true } + initialNumToRender={initialNumToRender} + windowSize={11} /> ) From 1cf49517b517e8bee90bf937d033dff35cc9f690 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 7 Mar 2024 09:04:02 -0800 Subject: [PATCH 16/29] Allow all encoding for hashtags in URL (#3131) --- src/components/TagMenu/index.tsx | 4 ++-- src/components/TagMenu/index.web.tsx | 4 ++-- src/screens/Hashtag.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/TagMenu/index.tsx b/src/components/TagMenu/index.tsx index c9ced9a54c..849a3f42d1 100644 --- a/src/components/TagMenu/index.tsx +++ b/src/components/TagMenu/index.tsx @@ -98,7 +98,7 @@ export function TagMenu({ control.close(() => { navigation.push('Hashtag', { - tag: tag.replaceAll('#', '%23'), + tag: encodeURIComponent(tag), }) }) @@ -153,7 +153,7 @@ export function TagMenu({ control.close(() => { navigation.push('Hashtag', { - tag: tag.replaceAll('#', '%23'), + tag: encodeURIComponent(tag), author: authorHandle, }) }) diff --git a/src/components/TagMenu/index.web.tsx b/src/components/TagMenu/index.web.tsx index a0dc2bce63..8245bd0191 100644 --- a/src/components/TagMenu/index.web.tsx +++ b/src/components/TagMenu/index.web.tsx @@ -66,7 +66,7 @@ export function TagMenu({ label: _(msg`See ${truncatedTag} posts`), onPress() { navigation.push('Hashtag', { - tag: tag.replaceAll('#', '%23'), + tag: encodeURIComponent(tag), }) }, testID: 'tagMenuSearch', @@ -83,7 +83,7 @@ export function TagMenu({ label: _(msg`See ${truncatedTag} posts by user`), onPress() { navigation.push('Hashtag', { - tag: tag.replaceAll('#', '%23'), + tag: encodeURIComponent(tag), author: authorHandle, }) }, diff --git a/src/screens/Hashtag.tsx b/src/screens/Hashtag.tsx index f1b8173705..776cc585e3 100644 --- a/src/screens/Hashtag.tsx +++ b/src/screens/Hashtag.tsx @@ -42,7 +42,7 @@ export default function HashtagScreen({ const [isPTR, setIsPTR] = React.useState(false) const fullTag = React.useMemo(() => { - return `#${tag.replaceAll('%23', '#')}` + return `#${decodeURIComponent(tag)}` }, [tag]) const queryParam = React.useMemo(() => { @@ -83,7 +83,7 @@ export default function HashtagScreen({ const onShare = React.useCallback(() => { const url = new URL('https://bsky.app') - url.pathname = `/hashtag/${tag}` + url.pathname = `/hashtag/${decodeURIComponent(tag)}` if (author) { url.searchParams.set('author', author) } From c8e0fa9c97d276a917996627f163c267783e9411 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 8 Mar 2024 04:13:36 +0000 Subject: [PATCH 17/29] Mark bundle start time on web (#3147) * Mark bundle start time on web * TS --- index.web.js | 2 ++ src/platform/markBundleStartTime.web.ts | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 src/platform/markBundleStartTime.web.ts diff --git a/index.web.js b/index.web.js index 4dee831cda..9623734512 100644 --- a/index.web.js +++ b/index.web.js @@ -1,3 +1,5 @@ +import '#/platform/markBundleStartTime' + import '#/platform/polyfills' import {registerRootComponent} from 'expo' import {doPolyfill} from '#/lib/api/api-polyfill' diff --git a/src/platform/markBundleStartTime.web.ts b/src/platform/markBundleStartTime.web.ts new file mode 100644 index 0000000000..cd64c9f1c3 --- /dev/null +++ b/src/platform/markBundleStartTime.web.ts @@ -0,0 +1,2 @@ +// @ts-ignore Web-only. On RN, this is set by Metro. +window.__BUNDLE_START_TIME__ = performance.now() From 31826633cb1d1180875b20e218a39ce341ab2ec0 Mon Sep 17 00:00:00 2001 From: Hailey Date: Thu, 7 Mar 2024 20:14:24 -0800 Subject: [PATCH 18/29] rm waitlist modal, button during sign up (#3148) --- src/view/com/auth/create/Step1.tsx | 27 +--- src/view/com/modals/Modal.tsx | 4 - src/view/com/modals/Modal.web.tsx | 3 - src/view/com/modals/Waitlist.tsx | 190 ----------------------------- 4 files changed, 2 insertions(+), 222 deletions(-) delete mode 100644 src/view/com/modals/Waitlist.tsx diff --git a/src/view/com/auth/create/Step1.tsx b/src/view/com/auth/create/Step1.tsx index 4c70184850..c1bec535f5 100644 --- a/src/view/com/auth/create/Step1.tsx +++ b/src/view/com/auth/create/Step1.tsx @@ -4,7 +4,6 @@ import { Keyboard, StyleSheet, TouchableOpacity, - TouchableWithoutFeedback, View, } from 'react-native' import {CreateAccountState, CreateAccountDispatch, is18} from './state' @@ -19,7 +18,6 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import {isWeb} from 'platform/detection' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' import {logger} from '#/logger' import { FontAwesomeIcon, @@ -49,7 +47,6 @@ export function Step1({ }) { const pal = usePalette('default') const {_} = useLingui() - const {openModal} = useModalControls() const serverInputControl = useDialogControl() const onPressSelectService = React.useCallback(() => { @@ -57,10 +54,6 @@ export function Step1({ Keyboard.dismiss() }, [serverInputControl]) - const onPressWaitlist = React.useCallback(() => { - openModal({name: 'waitlist'}) - }, [openModal]) - const birthDate = React.useMemo(() => { return sanitizeDate(uiState.birthDate) }, [uiState.birthDate]) @@ -164,23 +157,7 @@ export function Step1({ )} - {!uiState.inviteCode && uiState.isInviteCodeRequired ? ( - - - Don't have an invite code?{' '} - - - - - Join the waitlist. - - - - - ) : ( + {uiState.inviteCode ? ( <> )} - )} + ) : undefined} )} diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 8da91c75cf..100444ff58 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -20,7 +20,6 @@ import * as ReportModal from './report/Modal' import * as AppealLabelModal from './AppealLabel' import * as DeleteAccountModal from './DeleteAccount' import * as ChangeHandleModal from './ChangeHandle' -import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' @@ -109,9 +108,6 @@ export function ModalsContainer() { } else if (activeModal?.name === 'change-handle') { snapPoints = ChangeHandleModal.snapPoints element = - } else if (activeModal?.name === 'waitlist') { - snapPoints = WaitlistModal.snapPoints - element = } else if (activeModal?.name === 'invite-codes') { snapPoints = InviteCodesModal.snapPoints element = diff --git a/src/view/com/modals/Modal.web.tsx b/src/view/com/modals/Modal.web.tsx index 97a60be913..0ced894a17 100644 --- a/src/view/com/modals/Modal.web.tsx +++ b/src/view/com/modals/Modal.web.tsx @@ -22,7 +22,6 @@ import * as CropImageModal from './crop-image/CropImage.web' import * as AltTextImageModal from './AltImage' import * as EditImageModal from './EditImage' import * as ChangeHandleModal from './ChangeHandle' -import * as WaitlistModal from './Waitlist' import * as InviteCodesModal from './InviteCodes' import * as AddAppPassword from './AddAppPasswords' import * as ContentFilteringSettingsModal from './ContentFilteringSettings' @@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) { element = } else if (modal.name === 'change-handle') { element = - } else if (modal.name === 'waitlist') { - element = } else if (modal.name === 'invite-codes') { element = } else if (modal.name === 'add-app-password') { diff --git a/src/view/com/modals/Waitlist.tsx b/src/view/com/modals/Waitlist.tsx deleted file mode 100644 index 263dd27a2f..0000000000 --- a/src/view/com/modals/Waitlist.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react' -import { - ActivityIndicator, - StyleSheet, - TouchableOpacity, - View, -} from 'react-native' -import {TextInput} from './util' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' -import LinearGradient from 'react-native-linear-gradient' -import {Text} from '../util/text/Text' -import {s, gradients} from 'lib/styles' -import {usePalette} from 'lib/hooks/usePalette' -import {useTheme} from 'lib/ThemeContext' -import {ErrorMessage} from '../util/error/ErrorMessage' -import {cleanError} from 'lib/strings/errors' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' -import {useModalControls} from '#/state/modals' - -export const snapPoints = ['80%'] - -export function Component({}: {}) { - const pal = usePalette('default') - const theme = useTheme() - const {_} = useLingui() - const {closeModal} = useModalControls() - const [email, setEmail] = React.useState('') - const [isEmailSent, setIsEmailSent] = React.useState(false) - const [isProcessing, setIsProcessing] = React.useState(false) - const [error, setError] = React.useState('') - - const onPressSignup = async () => { - setError('') - setIsProcessing(true) - try { - const res = await fetch('https://bsky.app/api/waitlist', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({email}), - }) - const resBody = await res.json() - if (resBody.success) { - setIsEmailSent(true) - } else { - setError( - resBody.error || - _(msg`Something went wrong. Check your email and try again.`), - ) - } - } catch (e: any) { - setError(cleanError(e)) - } - setIsProcessing(false) - } - const onCancel = () => { - closeModal() - } - - return ( - - - - Join the waitlist - - - - Bluesky uses invites to build a healthier community. If you don't - know anybody with an invite, you can sign up for the waitlist and - we'll send one soon. - - - - {error ? ( - - - - ) : undefined} - {isProcessing ? ( - - - - ) : isEmailSent ? ( - - - - - Your email has been saved! We'll be in touch soon. - - - - ) : ( - <> - - - - Join Waitlist - - - - - - Cancel - - - - )} - - - ) -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - innerContainer: { - paddingBottom: 20, - }, - title: { - textAlign: 'center', - marginTop: 12, - marginBottom: 12, - }, - description: { - textAlign: 'center', - paddingHorizontal: 22, - marginBottom: 10, - }, - textInput: { - borderWidth: 1, - borderRadius: 6, - paddingHorizontal: 16, - paddingVertical: 12, - fontSize: 20, - marginHorizontal: 20, - }, - btn: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - borderRadius: 32, - padding: 14, - marginHorizontal: 20, - }, - error: { - borderRadius: 6, - marginHorizontal: 20, - marginBottom: 20, - }, -}) From dd86d0964d391e9748843aa1a6400d73c3a6d9f9 Mon Sep 17 00:00:00 2001 From: dan Date: Fri, 8 Mar 2024 04:33:42 +0000 Subject: [PATCH 19/29] Enable gating and experimentation on native, send init event (#3149) * Add the mobile fork * Add init event --- src/Navigation.tsx | 4 +++ src/lib/statsig/statsig.tsx | 62 ++++++++++++++++++++++++++++++--- src/lib/statsig/statsig.web.tsx | 14 +++++++- 3 files changed, 74 insertions(+), 6 deletions(-) diff --git a/src/Navigation.tsx b/src/Navigation.tsx index b30f8f9822..8a9f69b5de 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -78,6 +78,7 @@ import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStack import {msg} from '@lingui/macro' import {i18n, MessageDescriptor} from '@lingui/core' import HashtagScreen from '#/screens/Hashtag' +import {logEvent} from './lib/statsig/statsig' const navigationRef = createNavigationContainerRef() @@ -649,11 +650,14 @@ function logModuleInitTime() { return } didInit = true + const initMs = Math.round( // @ts-ignore Emitted by Metro in the bundle prelude performance.now() - global.__BUNDLE_START_TIME__, ) console.log(`Time to first paint: ${initMs} ms`) + logEvent('init', initMs) + if (__DEV__) { // This log is noisy, so keep false committed const shouldLog = false diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx index 88a57c3fcd..43822dda85 100644 --- a/src/lib/statsig/statsig.tsx +++ b/src/lib/statsig/statsig.tsx @@ -1,11 +1,63 @@ import React from 'react' +import { + Statsig, + StatsigProvider, + useGate as useStatsigGate, +} from 'statsig-react-native-expo' +import {useSession} from '../../state/session' +import {sha256} from 'js-sha256' -export function useGate(_gateName: string) { - // Not enabled for native yet. - return false +const statsigOptions = { + environment: { + tier: process.env.NODE_ENV === 'development' ? 'development' : 'production', + }, + // Don't block on waiting for network. The fetched config will kick in on next load. + // This ensures the UI is always consistent and doesn't update mid-session. + // Note this makes cold load (no local storage) and private mode return `false` for all gates. + initTimeoutMs: 1, +} + +export function logEvent( + eventName: string, + value?: string | number | null, + metadata?: Record | null, +) { + Statsig.logEvent(eventName, value, metadata) +} + +export function useGate(gateName: string) { + const {isLoading, value} = useStatsigGate(gateName) + if (isLoading) { + // This should not happen because of waitForInitialization={true}. + console.error('Did not expected isLoading to ever be true.') + } + return value +} + +function toStatsigUser(did: string | undefined) { + let userID: string | undefined + if (did) { + userID = sha256(did) + } + return {userID} } export function Provider({children}: {children: React.ReactNode}) { - // Not enabled for native yet. - return children + const {currentAccount} = useSession() + const currentStatsigUser = React.useMemo( + () => toStatsigUser(currentAccount?.did), + [currentAccount?.did], + ) + return ( + + {children} + + ) } diff --git a/src/lib/statsig/statsig.web.tsx b/src/lib/statsig/statsig.web.tsx index 6508131c44..fc66e8d9fd 100644 --- a/src/lib/statsig/statsig.web.tsx +++ b/src/lib/statsig/statsig.web.tsx @@ -1,5 +1,9 @@ import React from 'react' -import {StatsigProvider, useGate as useStatsigGate} from 'statsig-react' +import { + Statsig, + StatsigProvider, + useGate as useStatsigGate, +} from 'statsig-react' import {useSession} from '../../state/session' import {sha256} from 'js-sha256' @@ -13,6 +17,14 @@ const statsigOptions = { initTimeoutMs: 1, } +export function logEvent( + eventName: string, + value?: string | number | null, + metadata?: Record | null, +) { + Statsig.logEvent(eventName, value, metadata) +} + export function useGate(gateName: string) { const {isLoading, value} = useStatsigGate(gateName) if (isLoading) { From 8f623c3bdf8dbbdc4c4f10f19b0b2c134b4160cb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 8 Mar 2024 14:45:59 -0600 Subject: [PATCH 20/29] Refactor `PostDropdownBtn` to use new `Menu` (#3112) * Refactor PostDropdownBtn (cherry picked from commit 0adf6cb75e3d4b7c1630cf6153c0d7e289e1b859) * Update icons (cherry picked from commit ac89ef9b28721c00736b1388455f3f5f092de0ad) * Port over fixes * fix scrollbar disappearing * Try CSS solution * Disable arrow for now --------- Co-authored-by: Hailey --- ...bubbleQuestion_stroke2_corner0_rounded.svg | 1 + .../icons/filter_stroke2_corner0_rounded.svg | 1 + ...akerVolumeFull_stroke2_corner0_rounded.svg | 1 + .../icons/trash_stroke2_corner0_rounded.svg | 1 + .../icons/warning_stroke2_corner0_rounded.svg | 1 + src/components/Menu/index.web.tsx | 8 +- src/components/icons/Bubble.tsx | 5 + src/components/icons/Filter.tsx | 5 + src/components/icons/Speaker.tsx | 5 + src/components/icons/Trash.tsx | 5 + src/components/icons/Warning.tsx | 5 + src/view/com/util/EventStopper.tsx | 11 +- src/view/com/util/forms/PostDropdownBtn.tsx | 376 ++++++++++-------- src/view/com/util/post-ctrls/PostCtrls.tsx | 1 + 14 files changed, 249 insertions(+), 177 deletions(-) create mode 100644 assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg create mode 100644 assets/icons/filter_stroke2_corner0_rounded.svg create mode 100644 assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg create mode 100644 assets/icons/trash_stroke2_corner0_rounded.svg create mode 100644 assets/icons/warning_stroke2_corner0_rounded.svg create mode 100644 src/components/icons/Bubble.tsx create mode 100644 src/components/icons/Filter.tsx create mode 100644 src/components/icons/Speaker.tsx create mode 100644 src/components/icons/Trash.tsx create mode 100644 src/components/icons/Warning.tsx diff --git a/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..0bfcc48a0e --- /dev/null +++ b/assets/icons/bubbleQuestion_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/filter_stroke2_corner0_rounded.svg b/assets/icons/filter_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..1fbcfc5711 --- /dev/null +++ b/assets/icons/filter_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..81357a12e3 --- /dev/null +++ b/assets/icons/speakerVolumeFull_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/trash_stroke2_corner0_rounded.svg b/assets/icons/trash_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d4b32f81fe --- /dev/null +++ b/assets/icons/trash_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/warning_stroke2_corner0_rounded.svg b/assets/icons/warning_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..d5b6f13d5f --- /dev/null +++ b/assets/icons/warning_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/Menu/index.web.tsx b/src/components/Menu/index.web.tsx index ca2e40566d..054e51b01e 100644 --- a/src/components/Menu/index.web.tsx +++ b/src/components/Menu/index.web.tsx @@ -92,10 +92,8 @@ export function Trigger({children, label, style}: TriggerProps) { accessibilityLabel={label} onFocus={onFocus} onBlur={onBlur} - style={flatten([style, web({outline: 0})])} - onPointerDown={() => { - control.open() - }} + style={flatten([style, focused && web({outline: 0})])} + onPointerDown={() => control.open()} {...web({ onMouseEnter, onMouseLeave, @@ -131,6 +129,7 @@ export function Outer({children}: React.PropsWithChildren<{}>) { {children} + {/* Disabled until we can fix positioning ) { .backgroundColor } /> + */} ) diff --git a/src/components/icons/Bubble.tsx b/src/components/icons/Bubble.tsx new file mode 100644 index 0000000000..d4e08f6d29 --- /dev/null +++ b/src/components/icons/Bubble.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const BubbleQuestion_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M5.002 17.036V5h14v12.036h-3.986a1 1 0 0 0-.639.23l-2.375 1.968-2.344-1.965a1 1 0 0 0-.643-.233H5.002ZM20.002 3h-16a1 1 0 0 0-1 1v14.036a1 1 0 0 0 1 1h4.65l2.704 2.266a1 1 0 0 0 1.28.004l2.74-2.27h4.626a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1Zm-7.878 3.663c-1.39 0-2.5 1.135-2.5 2.515a1 1 0 0 0 2 0c0-.294.232-.515.5-.515a.507.507 0 0 1 .489.6.174.174 0 0 1-.027.048 1.1 1.1 0 0 1-.267.226c-.508.345-1.128.923-1.286 1.978a1 1 0 1 0 1.978.297.762.762 0 0 1 .14-.359c.063-.086.155-.169.293-.262.436-.297 1.18-.885 1.18-2.013 0-1.38-1.11-2.515-2.5-2.515ZM12 15.75a1.25 1.25 0 1 1 0-2.5 1.25 1.25 0 0 1 0 2.5Z', +}) diff --git a/src/components/icons/Filter.tsx b/src/components/icons/Filter.tsx new file mode 100644 index 0000000000..02ac1c71b4 --- /dev/null +++ b/src/components/icons/Filter.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Filter_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M3 4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v4a1 1 0 0 1-.293.707L15 14.414V20a1 1 0 0 1-.758.97l-4 1A1 1 0 0 1 9 21v-6.586L3.293 8.707A1 1 0 0 1 3 8V4Zm2 1v2.586l5.707 5.707A1 1 0 0 1 11 14v5.72l2-.5V14a1 1 0 0 1 .293-.707L19 7.586V5H5Z', +}) diff --git a/src/components/icons/Speaker.tsx b/src/components/icons/Speaker.tsx new file mode 100644 index 0000000000..365d5e1143 --- /dev/null +++ b/src/components/icons/Speaker.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const SpeakerVolumeFull_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M12.472 3.118A1 1 0 0 1 13 4v16a1 1 0 0 1-1.555.832L5.697 17H2a1 1 0 0 1-1-1V8a1 1 0 0 1 1-1h3.697l5.748-3.832a1 1 0 0 1 1.027-.05ZM11 5.868 6.555 8.833A1 1 0 0 1 6 9H3v6h3a1 1 0 0 1 .555.168L11 18.131V5.87Zm7.364-1.645a1 1 0 0 1 1.414 0A10.969 10.969 0 0 1 23 12c0 3.037-1.232 5.788-3.222 7.778a1 1 0 1 1-1.414-1.414A8.969 8.969 0 0 0 21 12a8.969 8.969 0 0 0-2.636-6.364 1 1 0 0 1 0-1.414Zm-3.182 3.181a1 1 0 0 1 1.414 0A6.483 6.483 0 0 1 18.5 12a6.483 6.483 0 0 1-1.904 4.597 1 1 0 0 1-1.414-1.415A4.483 4.483 0 0 0 16.5 12a4.483 4.483 0 0 0-1.318-3.182 1 1 0 0 1 0-1.414Z', +}) diff --git a/src/components/icons/Trash.tsx b/src/components/icons/Trash.tsx new file mode 100644 index 0000000000..d09a3311ff --- /dev/null +++ b/src/components/icons/Trash.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Trash_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M7.416 5H3a1 1 0 0 0 0 2h1.064l.938 14.067A1 1 0 0 0 6 22h12a1 1 0 0 0 .998-.933L19.936 7H21a1 1 0 1 0 0-2h-4.416a5 5 0 0 0-9.168 0Zm2.348 0h4.472c-.55-.614-1.348-1-2.236-1-.888 0-1.687.386-2.236 1Zm6.087 2H6.07l.867 13h10.128l.867-13h-2.036a1 1 0 0 1-.044 0ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z', +}) diff --git a/src/components/icons/Warning.tsx b/src/components/icons/Warning.tsx new file mode 100644 index 0000000000..fc84b28945 --- /dev/null +++ b/src/components/icons/Warning.tsx @@ -0,0 +1,5 @@ +import {createSinglePathSVG} from './TEMPLATE' + +export const Warning_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z', +}) diff --git a/src/view/com/util/EventStopper.tsx b/src/view/com/util/EventStopper.tsx index e743e89bbe..8f5f5cf54d 100644 --- a/src/view/com/util/EventStopper.tsx +++ b/src/view/com/util/EventStopper.tsx @@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native' export function EventStopper({ children, style, -}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) { + onKeyDown = true, +}: React.PropsWithChildren<{ + style?: ViewStyle | ViewStyle[] + /** + * Default `true`. Set to `false` to allow onKeyDown to propagate + */ + onKeyDown?: boolean +}>) { const stop = (e: any) => { e.stopPropagation() } @@ -18,7 +25,7 @@ export function EventStopper({ onTouchEnd={stop} // @ts-ignore web only -prf onClick={stop} - onKeyDown={stop} + onKeyDown={onKeyDown ? stop : undefined} style={style}> {children} diff --git a/src/view/com/util/forms/PostDropdownBtn.tsx b/src/view/com/util/forms/PostDropdownBtn.tsx index 09850a7f54..6f2ae55b2c 100644 --- a/src/view/com/util/forms/PostDropdownBtn.tsx +++ b/src/view/com/util/forms/PostDropdownBtn.tsx @@ -1,5 +1,11 @@ import React, {memo} from 'react' -import {StyleProp, View, ViewStyle} from 'react-native' +import { + StyleProp, + ViewStyle, + Pressable, + View, + PressableProps, +} from 'react-native' import Clipboard from '@react-native-clipboard/clipboard' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {useNavigation} from '@react-navigation/native' @@ -12,10 +18,6 @@ import { import {toShareUrl} from 'lib/strings/url-helpers' import {useTheme} from 'lib/ThemeContext' import {shareUrl} from 'lib/sharing' -import { - NativeDropdown, - DropdownItem as NativeDropdownItem, -} from './NativeDropdown' import * as Toast from '../Toast' import {EventStopper} from '../EventStopper' import {useModalControls} from '#/state/modals' @@ -36,6 +38,19 @@ import {isWeb} from '#/platform/detection' import {richTextToString} from '#/lib/strings/rich-text-helpers' import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' +import {atoms as a, useTheme as useAlf, web} from '#/alf' +import * as Menu from '#/components/Menu' +import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard' +import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter' +import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox' +import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash' +import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute' +import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' +import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble' +import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning' +import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash' +import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' + let PostDropdownBtn = ({ testID, postAuthor, @@ -45,6 +60,7 @@ let PostDropdownBtn = ({ richText, style, showAppealLabelItem, + hitSlop, }: { testID: string postAuthor: AppBskyActorDefs.ProfileViewBasic @@ -54,9 +70,11 @@ let PostDropdownBtn = ({ richText: RichTextAPI style?: StyleProp showAppealLabelItem?: boolean + hitSlop?: PressableProps['hitSlop'] }): React.ReactNode => { const {hasSession, currentAccount} = useSession() const theme = useTheme() + const alf = useAlf() const {_} = useLingui() const defaultCtrlColor = theme.palette.default.postCtrl const {openModal} = useModalControls() @@ -151,173 +169,189 @@ let PostDropdownBtn = ({ hidePost({uri: postUri}) }, [postUri, hidePost]) - const dropdownItems: NativeDropdownItem[] = [ - { - label: _(msg`Translate`), - onPress() { - onOpenTranslate() - }, - testID: 'postDropdownTranslateBtn', - icon: { - ios: { - name: 'character.book.closed', - }, - android: 'ic_menu_sort_alphabetically', - web: 'language', - }, - }, - { - label: _(msg`Copy post text`), - onPress() { - onCopyPostText() - }, - testID: 'postDropdownCopyTextBtn', - icon: { - ios: { - name: 'doc.on.doc', - }, - android: 'ic_menu_edit', - web: ['far', 'paste'], - }, - }, - { - label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`), - onPress() { - const url = toShareUrl(href) - shareUrl(url) - }, - testID: 'postDropdownShareBtn', - icon: { - ios: { - name: 'square.and.arrow.up', - }, - android: 'ic_menu_share', - web: 'share', - }, - }, - hasSession && { - label: 'separator', - }, - hasSession && { - label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`), - onPress() { - onToggleThreadMute() - }, - testID: 'postDropdownMuteThreadBtn', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'comment-slash', - }, - }, - hasSession && { - label: _(msg`Mute words & tags`), - onPress() { - mutedWordsDialogControl.open() - }, - testID: 'postDropdownMuteWordsBtn', - icon: { - ios: { - name: 'speaker.slash', - }, - android: 'ic_lock_silent_mode', - web: 'filter', - }, - }, - hasSession && - !isAuthor && - !isPostHidden && { - label: _(msg`Hide post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Hide this post?`), - message: _(msg`This will hide this post from your feeds.`), - onPressConfirm: onHidePost, - }) - }, - testID: 'postDropdownHideBtn', - icon: { - ios: { - name: 'eye.slash', - }, - android: 'ic_menu_delete', - web: ['far', 'eye-slash'], - }, - }, - { - label: 'separator', - }, - !isAuthor && - hasSession && { - label: _(msg`Report post`), - onPress() { - openModal({ - name: 'report', - uri: postUri, - cid: postCid, - }) - }, - testID: 'postDropdownReportBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - isAuthor && { - label: _(msg`Delete post`), - onPress() { - openModal({ - name: 'confirm', - title: _(msg`Delete this post?`), - message: _(msg`Are you sure? This cannot be undone.`), - onPressConfirm: onDeletePost, - }) - }, - testID: 'postDropdownDeleteBtn', - icon: { - ios: { - name: 'trash', - }, - android: 'ic_menu_delete', - web: ['far', 'trash-can'], - }, - }, - showAppealLabelItem && { - label: 'separator', - }, - showAppealLabelItem && { - label: _(msg`Appeal content warning`), - onPress() { - openModal({name: 'appeal-label', uri: postUri, cid: postCid}) - }, - testID: 'postDropdownAppealBtn', - icon: { - ios: { - name: 'exclamationmark.triangle', - }, - android: 'ic_menu_report_image', - web: 'circle-exclamation', - }, - }, - ].filter(Boolean) as NativeDropdownItem[] - return ( - - - - - - + + + + {({props, state}) => { + const styles = [ + style, + a.rounded_full, + (state.hovered || state.focused || state.pressed) && [ + web({outline: 0}), + alf.atoms.bg_contrast_25, + ], + ] + return isWeb ? ( + + + + ) : ( + + + + ) + }} + + + + + + {_(msg`Translate`)} + + + + + {_(msg`Copy post text`)} + + + + { + const url = toShareUrl(href) + shareUrl(url) + }}> + + {isWeb ? _(msg`Copy link to post`) : _(msg`Share`)} + + + + + + {hasSession && ( + <> + + + + + + {isThreadMuted + ? _(msg`Unmute thread`) + : _(msg`Mute thread`)} + + + + + mutedWordsDialogControl.open()}> + {_(msg`Mute words & tags`)} + + + + {!isAuthor && !isPostHidden && ( + { + openModal({ + name: 'confirm', + title: _(msg`Hide this post?`), + message: _( + msg`This will hide this post from your feeds.`, + ), + onPressConfirm: onHidePost, + }) + }}> + {_(msg`Hide post`)} + + + )} + + + )} + + + + + {!isAuthor && ( + { + openModal({ + name: 'report', + uri: postUri, + cid: postCid, + }) + }}> + {_(msg`Report post`)} + + + )} + + {isAuthor && ( + { + openModal({ + name: 'confirm', + title: _(msg`Delete this post?`), + message: _(msg`Are you sure? This cannot be undone.`), + onPressConfirm: onDeletePost, + }) + }}> + {_(msg`Delete post`)} + + + )} + + {showAppealLabelItem && ( + <> + + + { + openModal({ + name: 'appeal-label', + uri: postUri, + cid: postCid, + }) + }}> + + {_(msg`Appeal content warning`)} + + + + + )} + + + ) } diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx index bd21ddda2f..b1ec32b3f5 100644 --- a/src/view/com/util/post-ctrls/PostCtrls.tsx +++ b/src/view/com/util/post-ctrls/PostCtrls.tsx @@ -231,6 +231,7 @@ let PostCtrls = ({ richText={richText} showAppealLabelItem={showAppealLabelItem} style={styles.btnPad} + hitSlop={big ? HITSLOP_20 : HITSLOP_10} /> From 0f9f08b1ef795215975c7b041d0e94a992d22124 Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:31:24 -0800 Subject: [PATCH 21/29] Fix reactivity of dialogs (Dialogs Pt. 1) (#3146) * Improve a11y on ios * Format * Remove android * Fix android * Revert some changes * use sharedvalue for `importantForAccessibility` * add back `isOpen` * fix some more types --------- Co-authored-by: Eric Bailey --- src/components/Dialog/context.ts | 6 ++-- src/components/Dialog/types.ts | 2 +- src/state/dialogs/index.tsx | 60 ++++++++++++++++++++++---------- src/view/shell/index.tsx | 19 +++------- 4 files changed, 49 insertions(+), 38 deletions(-) diff --git a/src/components/Dialog/context.ts b/src/components/Dialog/context.ts index 9b571e8e9c..859f8edd77 100644 --- a/src/components/Dialog/context.ts +++ b/src/components/Dialog/context.ts @@ -21,8 +21,7 @@ export function useDialogControl(): DialogOuterProps['control'] { open: () => {}, close: () => {}, }) - const {activeDialogs, openDialogs} = useDialogStateContext() - const isOpen = openDialogs.includes(id) + const {activeDialogs} = useDialogStateContext() React.useEffect(() => { activeDialogs.current.set(id, control) @@ -36,7 +35,6 @@ export function useDialogControl(): DialogOuterProps['control'] { () => ({ id, ref: control, - isOpen, open: () => { control.current.open() }, @@ -44,6 +42,6 @@ export function useDialogControl(): DialogOuterProps['control'] { control.current.close(cb) }, }), - [id, control, isOpen], + [id, control], ) } diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts index fa9398fe05..4fc60ec394 100644 --- a/src/components/Dialog/types.ts +++ b/src/components/Dialog/types.ts @@ -22,7 +22,7 @@ export type DialogControlRefProps = { export type DialogControlProps = DialogControlRefProps & { id: string ref: React.RefObject - isOpen: boolean + isOpen?: boolean } export type DialogContextProps = { diff --git a/src/state/dialogs/index.tsx b/src/state/dialogs/index.tsx index 90aaca4f8b..951105a509 100644 --- a/src/state/dialogs/index.tsx +++ b/src/state/dialogs/index.tsx @@ -1,8 +1,9 @@ import React from 'react' +import {SharedValue, useSharedValue} from 'react-native-reanimated' import {DialogControlRefProps} from '#/components/Dialog' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' -const DialogContext = React.createContext<{ +interface IDialogContext { /** * The currently active `useDialogControl` hooks. */ @@ -13,13 +14,18 @@ const DialogContext = React.createContext<{ * The currently open dialogs, referenced by their IDs, generated from * `useId`. */ - openDialogs: string[] -}>({ - activeDialogs: { - current: new Map(), - }, - openDialogs: [], -}) + openDialogs: React.MutableRefObject> + /** + * The counterpart to `accessibilityViewIsModal` for Android. This property + * applies to the parent of all non-modal views, and prevents TalkBack from + * navigating within content beneath an open dialog. + * + * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android + */ + importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'> +} + +const DialogContext = React.createContext({} as IDialogContext) const DialogControlContext = React.createContext<{ closeAllDialogs(): boolean @@ -41,26 +47,42 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const activeDialogs = React.useRef< Map> >(new Map()) - const [openDialogs, setOpenDialogs] = React.useState([]) + const openDialogs = React.useRef>(new Set()) + const importantForAccessibility = useSharedValue< + 'auto' | 'no-hide-descendants' + >('auto') const closeAllDialogs = React.useCallback(() => { activeDialogs.current.forEach(dialog => dialog.current.close()) - return openDialogs.length > 0 - }, [openDialogs]) + return openDialogs.current.size > 0 + }, []) const setDialogIsOpen = React.useCallback( (id: string, isOpen: boolean) => { - setOpenDialogs(prev => { - const filtered = prev.filter(dialogId => dialogId !== id) as string[] - return isOpen ? [...filtered, id] : filtered - }) + if (isOpen) { + openDialogs.current.add(id) + importantForAccessibility.value = 'no-hide-descendants' + } else { + openDialogs.current.delete(id) + if (openDialogs.current.size < 1) { + importantForAccessibility.value = 'auto' + } + } }, - [setOpenDialogs], + [importantForAccessibility], ) - const context = React.useMemo( - () => ({activeDialogs, openDialogs}), - [openDialogs], + const context = React.useMemo( + () => ({ + activeDialogs: { + current: new Map(), + }, + openDialogs: { + current: new Set(), + }, + importantForAccessibility, + }), + [importantForAccessibility], ) const controls = React.useMemo( () => ({closeAllDialogs, setDialogIsOpen}), diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx index bdba791741..76a7f8fb3a 100644 --- a/src/view/shell/index.tsx +++ b/src/view/shell/index.tsx @@ -30,7 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util' import * as notifications from 'lib/notifications/notifications' import {Outlet as PortalOutlet} from '#/components/Portal' import {MutedWordsDialog} from '#/components/dialogs/MutedWords' -import {useDialogStateContext} from '#/state/dialogs' +import {useDialogStateContext} from 'state/dialogs' +import Animated from 'react-native-reanimated' function ShellInner() { const isDrawerOpen = useIsDrawerOpen() @@ -54,9 +55,9 @@ function ShellInner() { const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const {hasSession, currentAccount} = useSession() const closeAnyActiveElement = useCloseAnyActiveElement() + const {importantForAccessibility} = useDialogStateContext() // start undefined const currentAccountDid = React.useRef(undefined) - const {openDialogs} = useDialogStateContext() React.useEffect(() => { let listener = {remove() {}} @@ -80,19 +81,9 @@ function ShellInner() { } }, [currentAccount]) - /** - * The counterpart to `accessibilityViewIsModal` for Android. This property - * applies to the parent of all non-modal views, and prevents TalkBack from - * navigating within content beneath an open dialog. - * - * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android - */ - const importantForAccessibility = - openDialogs.length > 0 ? 'no-hide-descendants' : undefined - return ( <> - @@ -106,7 +97,7 @@ function ShellInner() { - + From 8ee325e73d927a34ef23a776c5404e9556f0d94a Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:31:50 -0800 Subject: [PATCH 22/29] Make ALF prompt scrollable for accessibility (#3150) * make alf prompt scrollable * padding --- src/components/Prompt.tsx | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 8e55bd8347..28ec2d0366 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useTheme, atoms as a} from '#/alf' +import {useTheme, atoms as a, useBreakpoints} from '#/alf' import {Text} from '#/components/Typography' import {Button} from '#/components/Button' @@ -25,6 +25,7 @@ export function Outer({ }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] }>) { + const {gtMobile} = useBreakpoints() const titleId = React.useId() const descriptionId = React.useId() @@ -38,12 +39,12 @@ export function Outer({ - + style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}> {children} - + ) @@ -71,8 +72,17 @@ export function Description({children}: React.PropsWithChildren<{}>) { } export function Actions({children}: React.PropsWithChildren<{}>) { + const {gtMobile} = useBreakpoints() + return ( - + {children} ) From 62e57c3b08020e17b3266876de342996c8bd12db Mon Sep 17 00:00:00 2001 From: Hailey Date: Fri, 8 Mar 2024 14:43:28 -0800 Subject: [PATCH 23/29] Adjustments to ALF prompt buttons (Dialogs Pt. 2) (#3144) * Improve a11y on ios * Format * Remove android * Fix android * small adjustment to buttons in prompt * full width below gtmobile * Revert some changes * use sharedvalue for `importantForAccessibility` * add back `isOpen` * fix some more types * small adjustment to buttons in prompt * full width below gtmobile --------- Co-authored-by: Eric Bailey --- src/components/Button.tsx | 4 +++- src/components/Prompt.tsx | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 5361be963a..d3bf73cc3e 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -27,7 +27,7 @@ export type ButtonColor = | 'gradient_sunset' | 'gradient_nordic' | 'gradient_bonfire' -export type ButtonSize = 'tiny' | 'small' | 'large' +export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large' export type ButtonShape = 'round' | 'square' | 'default' export type VariantProps = { /** @@ -274,6 +274,8 @@ export function Button({ if (shape === 'default') { if (size === 'large') { baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) + } else if (size === 'medium') { + baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md) } else if (size === 'small') { baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) } else if (size === 'tiny') { diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 28ec2d0366..3b245c440f 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -78,10 +78,9 @@ export function Actions({children}: React.PropsWithChildren<{}>) { {children} @@ -92,12 +91,13 @@ export function Cancel({ children, }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { const {_} = useLingui() + const {gtMobile} = useBreakpoints() const {close} = Dialog.useDialogContext() return (