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
diff --git a/app.config.js b/app.config.js
index 6ae059104e..a3303144de 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: {
@@ -131,10 +142,12 @@ 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),
extra: {
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/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})
-}
diff --git a/bskyweb/templates/base.html b/bskyweb/templates/base.html
index 413d7ff691..c7c5ec0f0b 100644
--- a/bskyweb/templates/base.html
+++ b/bskyweb/templates/base.html
@@ -43,6 +43,9 @@
height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges;
}
+ html, body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ }
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea {
@@ -213,6 +216,7 @@
}
/* NativeDropdown component */
+ .radix-dropdown-item:focus,
.nativeDropdown-item:focus {
outline: none;
}
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/package.json b/package.json
index c0c597e00e..785251ae67 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"
@@ -44,7 +44,7 @@
"update-extensions": "scripts/updateExtensions.sh"
},
"dependencies": {
- "@atproto/api": "^0.10.4",
+ "@atproto/api": "^0.10.5",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "https://github.com/bluesky-social/react-native-bottom-sheet.git#discord-fork-4.6.1",
"@emoji-mart/react": "^1.1.1",
@@ -57,6 +57,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.22.3",
"@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0",
@@ -143,8 +144,10 @@
"react": "18.2.0",
"react-avatar-editor": "^13.0.0",
"react-dom": "^18.2.0",
+ "react-keyed-flatten-children": "^3.0.0",
"react-native": "~0.73.5",
"react-native-date-picker": "^4.4.0",
+ "react-native-date-picker": "^4.4.0",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-gesture-handler": "~2.15.0",
"react-native-get-random-values": "~1.8.0",
@@ -166,6 +169,8 @@
"react-native-webview": "~13.8.1",
"react-responsive": "^9.0.2",
"sentry-expo": "~7.2.0",
+ "statsig-react": "^1.36.0",
+ "statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7",
"tlds": "^1.234.0",
"zeego": "^1.6.2",
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
+ })
+}
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 aae9b29d3f..5c4ac3380e 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -42,9 +42,12 @@ 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 {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()
@@ -68,26 +71,29 @@ function InnerApp() {
return (
+ {isAndroid && }
{/**/}
-
-
-
-
- {/* 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/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/components/Button.tsx b/src/components/Button.tsx
index a6d0ee1dc2..33f580bc98 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/Dialog/context.ts b/src/components/Dialog/context.ts
index eb717d8e2b..859f8edd77 100644
--- a/src/components/Dialog/context.ts
+++ b/src/components/Dialog/context.ts
@@ -31,14 +31,17 @@ 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,
+ open: () => {
+ control.current.open()
+ },
+ close: cb => {
+ control.current.close(cb)
+ },
+ }),
+ [id, control],
+ )
}
diff --git a/src/components/Dialog/types.ts b/src/components/Dialog/types.ts
index 7c8a6e26c6..4ee039dcb4 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/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/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..054e51b01e
--- /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}
+
+
+ {/* Disabled until we can fix positioning
+
+ */}
+
+
+ )
+}
+
+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/components/Prompt.tsx b/src/components/Prompt.tsx
index 8e55bd8347..3b245c440f 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,16 @@ export function Description({children}: React.PropsWithChildren<{}>) {
}
export function Actions({children}: React.PropsWithChildren<{}>) {
+ const {gtMobile} = useBreakpoints()
+
return (
-
+
{children}
)
@@ -82,12 +91,13 @@ export function Cancel({
children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
const {_} = useLingui()
+ const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext()
return (
)}
- {!uiState.inviteCode && uiState.isInviteCodeRequired ? (
-
-
- Don't have an invite code?{' '}
-
-
-
-
- Join the waitlist.
-
-
-
-
- ) : (
+ {!uiState.isInviteCodeRequired || uiState.inviteCode ? (
<>
)}
>
- )}
+ ) : undefined}
>
)}
diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx
index 8712a5019d..bebce9fdae 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
index b0df189836..e69de29bb2 100644
--- a/src/view/com/modals/Waitlist.tsx
+++ b/src/view/com/modals/Waitlist.tsx
@@ -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 'expo-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,
- },
-})
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}
/>
)
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/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 1ae485eb8d..325a6efc5a 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
}
diff --git a/src/view/com/util/forms/DateInput.tsx b/src/view/com/util/forms/DateInput.tsx
index 202e46fb95..0104562aa5 100644
--- a/src/view/com/util/forms/DateInput.tsx
+++ b/src/view/com/util/forms/DateInput.tsx
@@ -81,6 +81,7 @@ export function DateInput(props: Props) {
)}
{(isIOS || show) && (
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 +168,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/forms/SelectableBtn.tsx b/src/view/com/util/forms/SelectableBtn.tsx
index f09d063a15..e577e155de 100644
--- a/src/view/com/util/forms/SelectableBtn.tsx
+++ b/src/view/com/util/forms/SelectableBtn.tsx
@@ -57,6 +57,7 @@ const styles = StyleSheet.create({
btn: {
flexDirection: 'row',
justifyContent: 'center',
+ flexGrow: 1,
borderWidth: 1,
borderLeftWidth: 0,
paddingHorizontal: 10,
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index bd21ddda2f..1e26eeccee 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -212,9 +212,7 @@ let PostCtrls = ({
style={[styles.btn]}
onPress={onShare}
accessibilityRole="button"
- accessibilityLabel={`${
- post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
- } (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
+ accessibilityLabel={`${_(msg`Share`)}`}
accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
@@ -231,6 +229,7 @@ let PostCtrls = ({
richText={richText}
showAppealLabelItem={showAppealLabelItem}
style={styles.btnPad}
+ hitSlop={big ? HITSLOP_20 : HITSLOP_10}
/>
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/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`)}
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() {
-
+
diff --git a/web/index.html b/web/index.html
index 8f2275a7f4..de0abfc91b 100644
--- a/web/index.html
+++ b/web/index.html
@@ -47,6 +47,9 @@
height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges;
}
+ html, body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ }
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea {
@@ -217,6 +220,7 @@
}
/* NativeDropdown component */
+ .radix-dropdown-item:focus,
.nativeDropdown-item:focus {
outline: none;
}
diff --git a/yarn.lock b/yarn.lock
index 8dcdddee43..7c431fd146 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"
@@ -3119,6 +3119,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"
@@ -3142,6 +3163,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"
@@ -3164,6 +3190,23 @@
slugify "^1.3.4"
sucrase "3.34.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"
@@ -3263,6 +3306,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"
@@ -3358,6 +3410,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"
@@ -4479,6 +4540,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"
@@ -4493,6 +4566,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"
@@ -4510,6 +4597,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"
@@ -4543,6 +4640,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"
@@ -4560,6 +4682,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"
@@ -4568,6 +4707,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"
@@ -4669,6 +4816,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-async-storage/async-storage@^1.22.3":
version "1.22.3"
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.3.tgz#ad490236a9eda8ac68cffc12c738f20b1815464e"
@@ -8028,7 +8182,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==
@@ -14767,6 +14921,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"
@@ -14951,7 +15115,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==
@@ -18251,6 +18415,18 @@ 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-date-picker@^4.4.0:
+ version "4.4.0"
+ resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-4.4.0.tgz#fe5b6eb8d85a4a30b2991ada5169a30ce5023ead"
+ integrity sha512-Axx3byihwwhKRLRVjPAr/UaEysapkRcKmjjM8/05UaVm4Q0xDn2RFUcRdy1QAahhRcjLjlVYhepxvU5bdgy7ZQ==
+
react-native-date-picker@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/react-native-date-picker/-/react-native-date-picker-4.4.0.tgz#fe5b6eb8d85a4a30b2991ada5169a30ce5023ead"
@@ -18281,6 +18457,13 @@ react-native-gesture-handler@~2.15.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"
@@ -19722,6 +19905,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"
@@ -20661,6 +20887,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"
@@ -20930,7 +21161,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==
@@ -21668,6 +21899,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"