Merge branch 'bluesky-social:main' into main

zio/stable
Jan-Olof Eriksson 2024-03-11 14:52:33 +02:00 committed by GitHub
commit 4a2251f48b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
70 changed files with 1720 additions and 753 deletions

View File

@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
nvm use $NODE_VERSION && \ nvm use $NODE_VERSION && \
npm install --global yarn && \ npm install --global yarn && \
yarn && \ yarn && \
yarn intl:compile && \ yarn intl:build && \
yarn build-web yarn build-web
# DEBUG # DEBUG

View File

@ -11,6 +11,17 @@ const DARK_SPLASH_CONFIG = {
resizeMode: 'cover', 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) { module.exports = function (config) {
/** /**
* App version number. Should be incremented as part of a release cycle. * App version number. Should be incremented as part of a release cycle.
@ -70,8 +81,8 @@ module.exports = function (config) {
}, },
}, },
androidStatusBar: { androidStatusBar: {
barStyle: 'dark-content', barStyle: 'light-content',
backgroundColor: '#ffffff', backgroundColor: '#00000000',
}, },
android: { android: {
icon: './assets/icon.png', icon: './assets/icon.png',
@ -101,8 +112,8 @@ module.exports = function (config) {
}, },
], ],
splash: { splash: {
...SPLASH_CONFIG, ...SPLASH_CONFIG_ANDROID,
dark: DARK_SPLASH_CONFIG, dark: DARK_SPLASH_CONFIG_ANDROID,
}, },
}, },
web: { web: {
@ -121,12 +132,14 @@ module.exports = function (config) {
{ {
ios: { ios: {
deploymentTarget: '13.4', deploymentTarget: '13.4',
newArchEnabled: false,
}, },
android: { android: {
compileSdkVersion: 34, compileSdkVersion: 34,
targetSdkVersion: 34, targetSdkVersion: 34,
buildToolsVersion: '34.0.0', buildToolsVersion: '34.0.0',
kotlinVersion: '1.8.0', kotlinVersion: '1.8.0',
newArchEnabled: false,
}, },
}, },
], ],
@ -140,10 +153,12 @@ module.exports = function (config) {
'expo-notifications', 'expo-notifications',
{ {
icon: './assets/icon-android-notification.png', icon: './assets/icon-android-notification.png',
color: '#ffffff', color: '#1185fe',
}, },
], ],
'./plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestPlugin.js',
'./plugins/withAndroidManifestFCMIconPlugin.js',
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
'./plugins/shareExtension/withShareExtensions.js', './plugins/shareExtension/withShareExtensions.js',
].filter(Boolean), ].filter(Boolean),
extra: { extra: {

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 738 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 371 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 717 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 508 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 503 B

View File

@ -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),
})
}

View File

@ -40,18 +40,6 @@ func run(args []string) {
// retain old PDS env var for easy transition // retain old PDS env var for easy transition
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"}, 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{ &cli.StringFlag{
Name: "http-address", Name: "http-address",
Usage: "Specify the local IP/port to bind to", Usage: "Specify the local IP/port to bind to",

View File

@ -2,11 +2,9 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io/fs" "io/fs"
"io/ioutil"
"net/http" "net/http"
"os" "os"
"os/signal" "os/signal"
@ -29,25 +27,19 @@ import (
) )
type Server struct { type Server struct {
echo *echo.Echo echo *echo.Echo
httpd *http.Server httpd *http.Server
mailmodo *Mailmodo xrpcc *xrpc.Client
xrpcc *xrpc.Client
} }
func serve(cctx *cli.Context) error { func serve(cctx *cli.Context) error {
debug := cctx.Bool("debug") debug := cctx.Bool("debug")
httpAddress := cctx.String("http-address") httpAddress := cctx.String("http-address")
appviewHost := cctx.String("appview-host") appviewHost := cctx.String("appview-host")
mailmodoAPIKey := cctx.String("mailmodo-api-key")
mailmodoListName := cctx.String("mailmodo-list-name")
// Echo // Echo
e := echo.New() e := echo.New()
// Mailmodo client.
mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
// create a new session (no auth) // create a new session (no auth)
xrpcc := &xrpc.Client{ xrpcc := &xrpc.Client{
Client: cliutil.NewHttpClient(), Client: cliutil.NewHttpClient(),
@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error {
// server // server
// //
server := &Server{ server := &Server{
echo: e, echo: e,
mailmodo: mailmodo, xrpcc: xrpcc,
xrpcc: xrpcc,
} }
// Create the HTTP server. // 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/liked-by", server.WebGeneric)
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric) e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
// Mailmodo
e.POST("/api/waitlist", server.apiWaitlist)
// Start the server. // Start the server.
log.Infof("starting server address=%s", httpAddress) log.Infof("starting server address=%s", httpAddress)
go func() { go func() {
@ -398,36 +386,3 @@ func (srv *Server) WebProfile(c echo.Context) error {
data["requestHost"] = req.Host data["requestHost"] = req.Host
return c.Render(http.StatusOK, "profile.html", data) 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})
}

View File

@ -43,6 +43,9 @@
height: calc(100% + env(safe-area-inset-top)); height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges; 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 */ /* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea { button, input, textarea {
@ -213,6 +216,7 @@
} }
/* NativeDropdown component */ /* NativeDropdown component */
.radix-dropdown-item:focus,
.nativeDropdown-item:focus { .nativeDropdown-item:focus {
outline: none; outline: none;
} }

View File

@ -1,3 +1,5 @@
import '#/platform/markBundleStartTime'
import '#/platform/polyfills' import '#/platform/polyfills'
import {registerRootComponent} from 'expo' import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill' import {doPolyfill} from '#/lib/api/api-polyfill'

View File

@ -1,6 +1,6 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.71.0", "version": "1.72.0",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@ -44,7 +44,7 @@
"update-extensions": "scripts/updateExtensions.sh" "update-extensions": "scripts/updateExtensions.sh"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.10.4", "@atproto/api": "^0.10.5",
"@bam.tech/react-native-image-resizer": "^3.0.4", "@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
"@emoji-mart/react": "^1.1.1", "@emoji-mart/react": "^1.1.1",
@ -58,11 +58,11 @@
"@lingui/react": "^4.5.0", "@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4", "@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1", "@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-async-storage/async-storage": "1.21.0",
"@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0", "@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.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-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0", "@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.6.1", "@react-native-picker/picker": "2.6.1",
@ -148,8 +148,10 @@
"react-avatar-editor": "^13.0.0", "react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0", "react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-keyed-flatten-children": "^3.0.0",
"react-native": "0.73.2", "react-native": "0.73.2",
"react-native-appstate-hook": "^1.0.6", "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-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0", "react-native-fs": "^2.20.0",
"react-native-gesture-handler": "~2.14.0", "react-native-gesture-handler": "~2.14.0",
@ -177,6 +179,8 @@
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1", "sentry-expo": "~7.0.1",
"statsig-react": "^1.36.0",
"statsig-react-native-expo": "^4.6.1",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"tlds": "^1.234.0", "tlds": "^1.234.0",
"use-deep-compare": "^1.1.0", "use-deep-compare": "^1.1.0",

View File

@ -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
})
}

View File

@ -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
})
}

View File

@ -43,9 +43,12 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {Splash} from '#/Splash' import {Splash} from '#/Splash'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useIntentHandler} from 'lib/hooks/useIntentHandler' import {useIntentHandler} from 'lib/hooks/useIntentHandler'
import {StatusBar} from 'expo-status-bar'
import {isAndroid} from 'platform/detection'
SplashScreen.preventAutoHideAsync() SplashScreen.preventAutoHideAsync()
@ -69,26 +72,29 @@ function InnerApp() {
return ( return (
<SafeAreaProvider initialMetrics={initialWindowMetrics}> <SafeAreaProvider initialMetrics={initialWindowMetrics}>
{isAndroid && <StatusBar />}
<Alf theme={theme}> <Alf theme={theme}>
<Splash isReady={!isInitialLoad}> <Splash isReady={!isInitialLoad}>
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<LoggedOutViewProvider> <StatsigProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<ThemeProvider theme={theme}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={theme}>
<RootSiblingParent> {/* All components should be within this provider */}
<GestureHandlerRootView style={s.h100pct}> <RootSiblingParent>
<TestCtrls /> <GestureHandlerRootView style={s.h100pct}>
<Shell /> <TestCtrls />
</GestureHandlerRootView> <Shell />
</RootSiblingParent> </GestureHandlerRootView>
</ThemeProvider> </RootSiblingParent>
</UnreadNotifsProvider> </ThemeProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</LoggedOutViewProvider>
</StatsigProvider>
</React.Fragment> </React.Fragment>
</Splash> </Splash>
</Alf> </Alf>

View File

@ -32,6 +32,7 @@ import {
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread' import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {Provider as PortalProvider} from '#/components/Portal' import {Provider as PortalProvider} from '#/components/Portal'
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
import {useIntentHandler} from 'lib/hooks/useIntentHandler' import {useIntentHandler} from 'lib/hooks/useIntentHandler'
function InnerApp() { function InnerApp() {
@ -54,21 +55,23 @@ function InnerApp() {
<React.Fragment <React.Fragment
// Resets the entire tree below when it changes: // Resets the entire tree below when it changes:
key={currentAccount?.did}> key={currentAccount?.did}>
<LoggedOutViewProvider> <StatsigProvider>
<SelectedFeedProvider> <LoggedOutViewProvider>
<UnreadNotifsProvider> <SelectedFeedProvider>
<ThemeProvider theme={theme}> <UnreadNotifsProvider>
{/* All components should be within this provider */} <ThemeProvider theme={theme}>
<RootSiblingParent> {/* All components should be within this provider */}
<SafeAreaProvider> <RootSiblingParent>
<Shell /> <SafeAreaProvider>
</SafeAreaProvider> <Shell />
</RootSiblingParent> </SafeAreaProvider>
<ToastContainer /> </RootSiblingParent>
</ThemeProvider> <ToastContainer />
</UnreadNotifsProvider> </ThemeProvider>
</SelectedFeedProvider> </UnreadNotifsProvider>
</LoggedOutViewProvider> </SelectedFeedProvider>
</LoggedOutViewProvider>
</StatsigProvider>
</React.Fragment> </React.Fragment>
</Alf> </Alf>
) )

View File

@ -78,6 +78,7 @@ import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStack
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {i18n, MessageDescriptor} from '@lingui/core' import {i18n, MessageDescriptor} from '@lingui/core'
import HashtagScreen from '#/screens/Hashtag' import HashtagScreen from '#/screens/Hashtag'
import {logEvent} from './lib/statsig/statsig'
const navigationRef = createNavigationContainerRef<AllNavigatorParams>() const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
@ -649,11 +650,14 @@ function logModuleInitTime() {
return return
} }
didInit = true didInit = true
const initMs = Math.round( const initMs = Math.round(
// @ts-ignore Emitted by Metro in the bundle prelude // @ts-ignore Emitted by Metro in the bundle prelude
performance.now() - global.__BUNDLE_START_TIME__, performance.now() - global.__BUNDLE_START_TIME__,
) )
console.log(`Time to first paint: ${initMs} ms`) console.log(`Time to first paint: ${initMs} ms`)
logEvent('init', initMs)
if (__DEV__) { if (__DEV__) {
// This log is noisy, so keep false committed // This log is noisy, so keep false committed
const shouldLog = false const shouldLog = false

View File

@ -27,7 +27,7 @@ export type ButtonColor =
| 'gradient_sunset' | 'gradient_sunset'
| 'gradient_nordic' | 'gradient_nordic'
| 'gradient_bonfire' | 'gradient_bonfire'
export type ButtonSize = 'tiny' | 'small' | 'large' export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
export type ButtonShape = 'round' | 'square' | 'default' export type ButtonShape = 'round' | 'square' | 'default'
export type VariantProps = { export type VariantProps = {
/** /**
@ -274,6 +274,8 @@ export function Button({
if (shape === 'default') { if (shape === 'default') {
if (size === 'large') { if (size === 'large') {
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md) 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') { } else if (size === 'small') {
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm) baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
} else if (size === 'tiny') { } else if (size === 'tiny') {

View File

@ -31,14 +31,17 @@ export function useDialogControl(): DialogOuterProps['control'] {
} }
}, [id, activeDialogs]) }, [id, activeDialogs])
return { return React.useMemo<DialogOuterProps['control']>(
id, () => ({
ref: control, id,
open: () => { ref: control,
control.current.open() open: () => {
}, control.current.open()
close: cb => { },
control.current.close(cb) close: cb => {
}, control.current.close(cb)
} },
}),
[id, control],
)
} }

View File

@ -15,7 +15,7 @@ import {useTheme, atoms as a, flatten} from '#/alf'
import {Portal} from '#/components/Portal' import {Portal} from '#/components/Portal'
import {createInput} from '#/components/forms/TextField' import {createInput} from '#/components/forms/TextField'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useDialogStateContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
import { import {
DialogOuterProps, DialogOuterProps,
@ -82,7 +82,7 @@ export function Outer({
const hasSnapPoints = !!sheetOptions.snapPoints const hasSnapPoints = !!sheetOptions.snapPoints
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const closeCallback = React.useRef<() => void>() const closeCallback = React.useRef<() => void>()
const {openDialogs} = useDialogStateContext() const {setDialogIsOpen} = useDialogStateControlContext()
/* /*
* Used to manage open/closed, but index is otherwise handled internally by `BottomSheet` * Used to manage open/closed, but index is otherwise handled internally by `BottomSheet`
@ -96,11 +96,11 @@ export function Outer({
const open = React.useCallback<DialogControlProps['open']>( const open = React.useCallback<DialogControlProps['open']>(
({index} = {}) => { ({index} = {}) => {
openDialogs.current.add(control.id) setDialogIsOpen(control.id, true)
// can be set to any index of `snapPoints`, but `0` is the first i.e. "open" // can be set to any index of `snapPoints`, but `0` is the first i.e. "open"
setOpenIndex(index || 0) setOpenIndex(index || 0)
}, },
[setOpenIndex, openDialogs, control.id], [setOpenIndex, setDialogIsOpen, control.id],
) )
const close = React.useCallback<DialogControlProps['close']>(cb => { const close = React.useCallback<DialogControlProps['close']>(cb => {
@ -119,65 +119,66 @@ export function Outer({
[open, close], [open, close],
) )
const onChange = React.useCallback( const onCloseInner = React.useCallback(() => {
(index: number) => { Keyboard.dismiss()
if (index === -1) { try {
Keyboard.dismiss() closeCallback.current?.()
try { } catch (e: any) {
closeCallback.current?.() logger.error(`Dialog closeCallback failed`, {
} catch (e: any) { message: e.message,
logger.error(`Dialog closeCallback failed`, { })
message: e.message, } finally {
}) closeCallback.current = undefined
} finally { }
closeCallback.current = undefined setDialogIsOpen(control.id, false)
} onClose?.()
setOpenIndex(-1)
openDialogs.current.delete(control.id) }, [control.id, onClose, setDialogIsOpen])
onClose?.()
setOpenIndex(-1)
}
},
[onClose, setOpenIndex, openDialogs, control.id],
)
const context = React.useMemo(() => ({close}), [close]) const context = React.useMemo(() => ({close}), [close])
return ( return (
isOpen && ( isOpen && (
<Portal> <Portal>
<BottomSheet <View
enableDynamicSizing={!hasSnapPoints} // iOS
enablePanDownToClose accessibilityViewIsModal
keyboardBehavior="interactive" // Android
android_keyboardInputMode="adjustResize" importantForAccessibility="yes"
keyboardBlurBehavior="restore" style={[a.absolute, a.inset_0]}>
topInset={insets.top} <BottomSheet
{...sheetOptions} enableDynamicSizing={!hasSnapPoints}
snapPoints={sheetOptions.snapPoints || ['100%']} enablePanDownToClose
ref={sheet} keyboardBehavior="interactive"
index={openIndex} android_keyboardInputMode="adjustResize"
backgroundStyle={{backgroundColor: 'transparent'}} keyboardBlurBehavior="restore"
backdropComponent={Backdrop} topInset={insets.top}
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}} {...sheetOptions}
handleStyle={{display: 'none'}} snapPoints={sheetOptions.snapPoints || ['100%']}
onChange={onChange}> ref={sheet}
<Context.Provider value={context}> index={openIndex}
<View backgroundStyle={{backgroundColor: 'transparent'}}
style={[ backdropComponent={Backdrop}
a.absolute, handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
a.inset_0, handleStyle={{display: 'none'}}
t.atoms.bg, onClose={onCloseInner}>
{ <Context.Provider value={context}>
borderTopLeftRadius: 40, <View
borderTopRightRadius: 40, style={[
height: Dimensions.get('window').height * 2, a.absolute,
}, a.inset_0,
]} t.atoms.bg,
/> {
{children} borderTopLeftRadius: 40,
</Context.Provider> borderTopRightRadius: 40,
</BottomSheet> height: Dimensions.get('window').height * 2,
},
]}
/>
{children}
</Context.Provider>
</BottomSheet>
</View>
</Portal> </Portal>
) )
) )

View File

@ -12,7 +12,7 @@ import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
import {Context} from '#/components/Dialog/context' import {Context} from '#/components/Dialog/context'
import {Button, ButtonIcon} from '#/components/Button' import {Button, ButtonIcon} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
import {useDialogStateContext} from '#/state/dialogs' import {useDialogStateControlContext} from '#/state/dialogs'
export {useDialogControl, useDialogContext} from '#/components/Dialog/context' export {useDialogControl, useDialogContext} from '#/components/Dialog/context'
export * from '#/components/Dialog/types' export * from '#/components/Dialog/types'
@ -30,21 +30,21 @@ export function Outer({
const {gtMobile} = useBreakpoints() const {gtMobile} = useBreakpoints()
const [isOpen, setIsOpen] = React.useState(false) const [isOpen, setIsOpen] = React.useState(false)
const [isVisible, setIsVisible] = React.useState(true) const [isVisible, setIsVisible] = React.useState(true)
const {openDialogs} = useDialogStateContext() const {setDialogIsOpen} = useDialogStateControlContext()
const open = React.useCallback(() => { const open = React.useCallback(() => {
setIsOpen(true) setIsOpen(true)
openDialogs.current.add(control.id) setDialogIsOpen(control.id, true)
}, [setIsOpen, openDialogs, control.id]) }, [setIsOpen, setDialogIsOpen, control.id])
const close = React.useCallback(async () => { const close = React.useCallback(async () => {
setIsVisible(false) setIsVisible(false)
await new Promise(resolve => setTimeout(resolve, 150)) await new Promise(resolve => setTimeout(resolve, 150))
setIsOpen(false) setIsOpen(false)
setIsVisible(true) setIsVisible(true)
openDialogs.current.delete(control.id) setDialogIsOpen(control.id, false)
onClose?.() onClose?.()
}, [onClose, setIsOpen, openDialogs, control.id]) }, [onClose, setIsOpen, setDialogIsOpen, control.id])
useImperativeHandle( useImperativeHandle(
control.ref, control.ref,

View File

@ -22,6 +22,7 @@ export type DialogControlRefProps = {
export type DialogControlProps = DialogControlRefProps & { export type DialogControlProps = DialogControlRefProps & {
id: string id: string
ref: React.RefObject<DialogControlRefProps> ref: React.RefObject<DialogControlRefProps>
isOpen?: boolean
} }
export type DialogContextProps = { export type DialogContextProps = {

View File

@ -1,6 +1,7 @@
import React from 'react' import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native' import {View} from 'react-native'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader' import {Loader} from '#/components/Loader'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {cleanError} from 'lib/strings/errors' import {cleanError} from 'lib/strings/errors'
@ -143,7 +144,7 @@ export function ListMaybePlaceholder({
}) { }) {
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const t = useTheme() const t = useTheme()
const {gtMobile} = useBreakpoints() const {gtMobile, gtTablet} = useBreakpoints()
const canGoBack = navigation.canGoBack() const canGoBack = navigation.canGoBack()
const onGoBack = React.useCallback(() => { const onGoBack = React.useCallback(() => {
@ -165,14 +166,16 @@ export function ListMaybePlaceholder({
if (!isEmpty) return null if (!isEmpty) return null
return ( return (
<View <CenteredView
style={[ style={[
a.flex_1, a.flex_1,
a.align_center, a.align_center,
!gtMobile ? [a.justify_between, a.border_t] : a.gap_5xl, !gtMobile ? a.justify_between : a.gap_5xl,
t.atoms.border_contrast_low, t.atoms.border_contrast_low,
{paddingTop: 175, paddingBottom: 110}, {paddingTop: 175, paddingBottom: 110},
]}> ]}
sideBorders={gtMobile}
topBorder={!gtTablet}>
{isLoading ? ( {isLoading ? (
<View style={[a.w_full, a.align_center, {top: 100}]}> <View style={[a.w_full, a.align_center, {top: 100}]}>
<Loader size="xl" /> <Loader size="xl" />
@ -241,6 +244,6 @@ export function ListMaybePlaceholder({
</View> </View>
</> </>
)} )}
</View> </CenteredView>
) )
} }

View File

@ -0,0 +1,8 @@
import React from 'react'
import type {ContextType} from '#/components/Menu/types'
export const Context = React.createContext<ContextType>({
// @ts-ignore
control: null,
})

View File

@ -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<ContextType>(
() => ({
control: control || defaultControl,
}),
[control, defaultControl],
)
return <Context.Provider value={context}>{children}</Context.Provider>
}
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 (
<Dialog.Outer control={context.control}>
<Dialog.Handle />
{/* Re-wrap with context since Dialogs are portal-ed to root */}
<Context.Provider value={context}>
<Dialog.ScrollableInner label="Menu TODO">
<View style={[a.gap_lg]}>{children}</View>
<View style={{height: a.gap_lg.gap}} />
</Dialog.ScrollableInner>
</Context.Provider>
</Dialog.Outer>
)
}
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 (
<Pressable
{...rest}
accessibilityHint=""
accessibilityLabel={label}
onPress={e => {
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}
</Pressable>
)
}
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
return (
<Text
numberOfLines={1}
ellipsizeMode="middle"
style={[
a.flex_1,
a.text_md,
a.font_bold,
t.atoms.text_contrast_medium,
{paddingTop: 3},
style,
]}>
{children}
</Text>
)
}
export function ItemIcon({icon: Comp}: ItemIconProps) {
const t = useTheme()
return <Comp size="lg" fill={t.atoms.text_contrast_medium.color} />
}
export function Group({children, style}: GroupProps) {
const t = useTheme()
return (
<View
style={[
a.rounded_md,
a.overflow_hidden,
a.border,
t.atoms.border_contrast_low,
style,
]}>
{flattenReactChildren(children).map((child, i) => {
return React.isValidElement(child) && child.type === Item ? (
<React.Fragment key={i}>
{i > 0 ? (
<View style={[a.border_b, t.atoms.border_contrast_low]} />
) : null}
{React.cloneElement(child, {
// @ts-ignore
style: {
borderRadius: 0,
borderWidth: 0,
},
})}
</React.Fragment>
) : null
})}
</View>
)
}
export function Divider() {
return null
}

View File

@ -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<ContextType>(
() => ({
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 (
<Context.Provider value={context}>
<DropdownMenu.Root
open={context.control.isOpen}
onOpenChange={onOpenChange}>
{children}
</DropdownMenu.Root>
</Context.Provider>
)
}
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 (
<DropdownMenu.Trigger asChild>
<Pressable
accessibilityHint=""
accessibilityLabel={label}
onFocus={onFocus}
onBlur={onBlur}
style={flatten([style, focused && web({outline: 0})])}
onPointerDown={() => control.open()}
{...web({
onMouseEnter,
onMouseLeave,
})}>
{children({
isNative: false,
control,
state: {
hovered,
focused,
pressed: false,
},
props: {},
})}
</Pressable>
</DropdownMenu.Trigger>
)
}
export function Outer({children}: React.PropsWithChildren<{}>) {
const t = useTheme()
return (
<DropdownMenu.Portal>
<DropdownMenu.Content sideOffset={5} loop aria-label="Test">
<View
style={[
a.rounded_sm,
a.p_xs,
t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25,
t.atoms.shadow_md,
]}>
{children}
</View>
{/* Disabled until we can fix positioning
<DropdownMenu.Arrow
className="DropdownMenuArrow"
fill={
(t.name === 'light' ? t.atoms.bg : t.atoms.bg_contrast_25)
.backgroundColor
}
/>
*/}
</DropdownMenu.Content>
</DropdownMenu.Portal>
)
}
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 (
<DropdownMenu.Item asChild>
<Pressable
{...rest}
className="radix-dropdown-item"
accessibilityHint=""
accessibilityLabel={label}
onPress={e => {
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}
</Pressable>
</DropdownMenu.Item>
)
}
export function ItemText({children, style}: ItemTextProps) {
const t = useTheme()
return (
<Text style={[a.flex_1, a.font_bold, t.atoms.text_contrast_high, style]}>
{children}
</Text>
)
}
export function ItemIcon({icon: Comp, position = 'left'}: ItemIconProps) {
const t = useTheme()
return (
<Comp
size="md"
fill={t.atoms.text_contrast_medium.color}
style={[
position === 'left' && {
marginLeft: -2,
},
position === 'right' && {
marginRight: -2,
marginLeft: 12,
},
]}
/>
)
}
export function Group({children}: GroupProps) {
return children
}
export function Divider() {
const t = useTheme()
return (
<DropdownMenu.Separator
style={flatten([
a.my_xs,
t.atoms.bg_contrast_100,
{
height: 1,
},
])}
/>
)
}

View File

@ -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<PressableProps, 'style'> &
ViewStyleProp & {
label: string
onPress: (e: GestureResponderEvent) => void
}
>
export type ItemTextProps = React.PropsWithChildren<TextStyleProp & {}>
export type ItemIconProps = React.PropsWithChildren<{
icon: React.ComponentType<SVGIconProps>
position?: 'left' | 'right'
}>
export type GroupProps = React.PropsWithChildren<ViewStyleProp & {}>

View File

@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' 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 {Text} from '#/components/Typography'
import {Button} from '#/components/Button' import {Button} from '#/components/Button'
@ -25,6 +25,7 @@ export function Outer({
}: React.PropsWithChildren<{ }: React.PropsWithChildren<{
control: Dialog.DialogOuterProps['control'] control: Dialog.DialogOuterProps['control']
}>) { }>) {
const {gtMobile} = useBreakpoints()
const titleId = React.useId() const titleId = React.useId()
const descriptionId = React.useId() const descriptionId = React.useId()
@ -38,12 +39,12 @@ export function Outer({
<Context.Provider value={context}> <Context.Provider value={context}>
<Dialog.Handle /> <Dialog.Handle />
<Dialog.Inner <Dialog.ScrollableInner
accessibilityLabelledBy={titleId} accessibilityLabelledBy={titleId}
accessibilityDescribedBy={descriptionId} accessibilityDescribedBy={descriptionId}
style={[{width: 'auto', maxWidth: 400}]}> style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
{children} {children}
</Dialog.Inner> </Dialog.ScrollableInner>
</Context.Provider> </Context.Provider>
</Dialog.Outer> </Dialog.Outer>
) )
@ -71,8 +72,16 @@ export function Description({children}: React.PropsWithChildren<{}>) {
} }
export function Actions({children}: React.PropsWithChildren<{}>) { export function Actions({children}: React.PropsWithChildren<{}>) {
const {gtMobile} = useBreakpoints()
return ( return (
<View style={[a.w_full, a.flex_row, a.gap_sm, a.justify_end]}> <View
style={[
a.w_full,
a.gap_sm,
a.justify_end,
gtMobile ? [a.flex_row] : [a.flex_col, a.pt_md, a.pb_4xl],
]}>
{children} {children}
</View> </View>
) )
@ -82,12 +91,13 @@ export function Cancel({
children, children,
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) { }: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
return ( return (
<Button <Button
variant="solid" variant="solid"
color="secondary" color="secondary"
size="small" size={gtMobile ? 'small' : 'medium'}
label={_(msg`Cancel`)} label={_(msg`Cancel`)}
onPress={() => close()}> onPress={() => close()}>
{children} {children}
@ -100,6 +110,7 @@ export function Action({
onPress, onPress,
}: React.PropsWithChildren<{onPress?: () => void}>) { }: React.PropsWithChildren<{onPress?: () => void}>) {
const {_} = useLingui() const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {close} = Dialog.useDialogContext() const {close} = Dialog.useDialogContext()
const handleOnPress = React.useCallback(() => { const handleOnPress = React.useCallback(() => {
close() close()
@ -109,7 +120,7 @@ export function Action({
<Button <Button
variant="solid" variant="solid"
color="primary" color="primary"
size="small" size={gtMobile ? 'small' : 'medium'}
label={_(msg`Confirm`)} label={_(msg`Confirm`)}
onPress={handleOnPress}> onPress={handleOnPress}>
{children} {children}

View File

@ -98,7 +98,7 @@ export function TagMenu({
control.close(() => { control.close(() => {
navigation.push('Hashtag', { navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'), tag: encodeURIComponent(tag),
}) })
}) })
@ -153,7 +153,7 @@ export function TagMenu({
control.close(() => { control.close(() => {
navigation.push('Hashtag', { navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'), tag: encodeURIComponent(tag),
author: authorHandle, author: authorHandle,
}) })
}) })

View File

@ -66,7 +66,7 @@ export function TagMenu({
label: _(msg`See ${truncatedTag} posts`), label: _(msg`See ${truncatedTag} posts`),
onPress() { onPress() {
navigation.push('Hashtag', { navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'), tag: encodeURIComponent(tag),
}) })
}, },
testID: 'tagMenuSearch', testID: 'tagMenuSearch',
@ -83,7 +83,7 @@ export function TagMenu({
label: _(msg`See ${truncatedTag} posts by user`), label: _(msg`See ${truncatedTag} posts by user`),
onPress() { onPress() {
navigation.push('Hashtag', { navigation.push('Hashtag', {
tag: tag.replaceAll('#', '%23'), tag: encodeURIComponent(tag),
author: authorHandle, author: authorHandle,
}) })
}, },

View File

@ -1,8 +1,5 @@
import React from 'react' import React from 'react'
import {View, Pressable} from 'react-native' import {View, Pressable} from 'react-native'
import DateTimePicker, {
BaseProps as DateTimePickerProps,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf' import {useTheme, atoms} from '#/alf'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
@ -15,6 +12,8 @@ import {
localizeDate, localizeDate,
toSimpleDateString, toSimpleDateString,
} from '#/components/forms/DateField/utils' } 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 * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const Label = TextField.Label
@ -38,20 +37,20 @@ export function DateField({
const {chromeFocus, chromeError, chromeErrorHover} = const {chromeFocus, chromeError, chromeErrorHover} =
TextField.useSharedInputStyles() TextField.useSharedInputStyles()
const onChangeInternal = React.useCallback< const onChangeInternal = React.useCallback(
Required<DateTimePickerProps>['onChange'] (date: Date) => {
>(
(_event, date) => {
setOpen(false) setOpen(false)
if (date) { const formatted = toSimpleDateString(date)
const formatted = toSimpleDateString(date) onChangeDate(formatted)
onChangeDate(formatted)
}
}, },
[onChangeDate, setOpen], [onChangeDate, setOpen],
) )
const onCancel = React.useCallback(() => {
setOpen(false)
}, [])
return ( return (
<View style={[atoms.relative, atoms.w_full]}> <View style={[atoms.relative, atoms.w_full]}>
<Pressable <Pressable
@ -89,18 +88,18 @@ export function DateField({
</Pressable> </Pressable>
{open && ( {open && (
<DateTimePicker <DatePicker
modal={isAndroid}
open={isAndroid}
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label} aria-label={label}
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={undefined} accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={t.name === 'light' ? 'light' : 'dark'}
value={new Date(value)}
onChange={onChangeInternal}
/> />
)} )}
</View> </View>

View File

@ -1,13 +1,11 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import DateTimePicker, {
DateTimePickerEvent,
} from '@react-native-community/datetimepicker'
import {useTheme, atoms} from '#/alf' import {useTheme, atoms} from '#/alf'
import * as TextField from '#/components/forms/TextField' import * as TextField from '#/components/forms/TextField'
import {toSimpleDateString} from '#/components/forms/DateField/utils' import {toSimpleDateString} from '#/components/forms/DateField/utils'
import {DateFieldProps} from '#/components/forms/DateField/types' import {DateFieldProps} from '#/components/forms/DateField/types'
import DatePicker from 'react-native-date-picker'
export * as utils from '#/components/forms/DateField/utils' export * as utils from '#/components/forms/DateField/utils'
export const Label = TextField.Label export const Label = TextField.Label
@ -28,7 +26,7 @@ export function DateField({
const t = useTheme() const t = useTheme()
const onChangeInternal = React.useCallback( const onChangeInternal = React.useCallback(
(event: DateTimePickerEvent, date: Date | undefined) => { (date: Date | undefined) => {
if (date) { if (date) {
const formatted = toSimpleDateString(date) const formatted = toSimpleDateString(date)
onChangeDate(formatted) onChangeDate(formatted)
@ -39,17 +37,15 @@ export function DateField({
return ( return (
<View style={[atoms.relative, atoms.w_full]}> <View style={[atoms.relative, atoms.w_full]}>
<DateTimePicker <DatePicker
theme={t.name === 'light' ? 'light' : 'dark'}
date={new Date(value)}
onDateChange={onChangeInternal}
mode="date"
testID={`${testID}-datepicker`}
aria-label={label} aria-label={label}
accessibilityLabel={label} accessibilityLabel={label}
accessibilityHint={undefined} accessibilityHint={undefined}
testID={`${testID}-datepicker`}
mode="date"
timeZoneName={'Etc/UTC'}
display="spinner"
themeVariant={t.name === 'light' ? 'light' : 'dark'}
value={new Date(value)}
onChange={onChangeInternal}
/> />
</View> </View>
) )

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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',
})

View File

@ -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])
}

View File

@ -2,9 +2,15 @@ import {RouteParams, Route} from './types'
export class Router { export class Router {
routes: [string, Route][] = [] routes: [string, Route][] = []
constructor(description: Record<string, string>) { constructor(description: Record<string, string | string[]>) {
for (const [screen, pattern] of Object.entries(description)) { for (const [screen, pattern] of Object.entries(description)) {
this.routes.push([screen, createRoute(pattern)]) if (typeof pattern === 'string') {
this.routes.push([screen, createRoute(pattern)])
} else {
pattern.forEach(subPattern => {
this.routes.push([screen, createRoute(subPattern)])
})
}
} }
} }

View File

@ -0,0 +1,75 @@
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'
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<string, string> | 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}) {
const {currentAccount} = useSession()
const currentStatsigUser = React.useMemo(
() => toStatsigUser(currentAccount?.did),
[currentAccount?.did],
)
React.useEffect(() => {
function refresh() {
// Intentionally refetching the config using the JS SDK rather than React SDK
// so that the new config is stored in cache but isn't used during this session.
// It will kick in for the next reload.
Statsig.updateUser(currentStatsigUser)
}
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
return () => clearInterval(id)
}, [currentStatsigUser])
return (
<StatsigProvider
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
mountKey={currentStatsigUser.userID}
user={currentStatsigUser}
// This isn't really blocking due to short initTimeoutMs above.
// However, it ensures `isLoading` is always `false`.
waitForInitialization={true}
options={statsigOptions}>
{children}
</StatsigProvider>
)
}

View File

@ -0,0 +1,75 @@
import React from 'react'
import {
Statsig,
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 logEvent(
eventName: string,
value?: string | number | null,
metadata?: Record<string, string> | 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}) {
const {currentAccount} = useSession()
const currentStatsigUser = React.useMemo(
() => toStatsigUser(currentAccount?.did),
[currentAccount?.did],
)
React.useEffect(() => {
function refresh() {
// Intentionally refetching the config using the JS SDK rather than React SDK
// so that the new config is stored in cache but isn't used during this session.
// It will kick in for the next reload.
Statsig.updateUser(currentStatsigUser)
}
const id = setInterval(refresh, 3 * 60e3 /* 3 min */)
return () => clearInterval(id)
}, [currentStatsigUser])
return (
<StatsigProvider
sdkKey="client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV"
mountKey={currentStatsigUser.userID}
user={currentStatsigUser}
// This isn't really blocking due to short initTimeoutMs above.
// However, it ensures `isLoading` is always `false`.
waitForInitialization={true}
options={statsigOptions}>
{children}
</StatsigProvider>
)
}

View File

@ -3,6 +3,8 @@ import {BSKY_SERVICE} from 'lib/constants'
import TLDs from 'tlds' import TLDs from 'tlds'
import psl from 'psl' import psl from 'psl'
export const BSKY_APP_HOST = 'https://bsky.app'
export function isValidDomain(str: string): boolean { export function isValidDomain(str: string): boolean {
return !!TLDs.find(tld => { return !!TLDs.find(tld => {
let i = str.lastIndexOf(tld) let i = str.lastIndexOf(tld)
@ -67,8 +69,21 @@ export function isBskyAppUrl(url: string): boolean {
return url.startsWith('https://bsky.app/') return url.startsWith('https://bsky.app/')
} }
export function isRelativeUrl(url: string): boolean {
return /^\/[^/]/.test(url)
}
export function isBskyRSSUrl(url: string): boolean {
return (
(url.startsWith('https://bsky.app/') || isRelativeUrl(url)) &&
/\/rss\/?$/.test(url)
)
}
export function isExternalUrl(url: string): boolean { export function isExternalUrl(url: string): boolean {
return !isBskyAppUrl(url) && url.startsWith('http') const external = !isBskyAppUrl(url) && url.startsWith('http')
const rss = isBskyRSSUrl(url)
return external || rss
} }
export function isBskyPostUrl(url: string): boolean { export function isBskyPostUrl(url: string): boolean {
@ -148,6 +163,11 @@ export function feedUriToHref(url: string): string {
export function linkRequiresWarning(uri: string, label: string) { export function linkRequiresWarning(uri: string, label: string) {
const labelDomain = labelToDomain(label) const labelDomain = labelToDomain(label)
// If the uri started with a / we know it is internal.
if (isRelativeUrl(uri)) {
return false
}
let urip let urip
try { try {
urip = new URL(uri) urip = new URL(uri)
@ -156,9 +176,12 @@ export function linkRequiresWarning(uri: string, label: string) {
} }
const host = urip.hostname.toLowerCase() const host = urip.hostname.toLowerCase()
// Hosts that end with bsky.app or bsky.social should be trusted by default. // Hosts that end with bsky.app or bsky.social should be trusted by default.
if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) { if (
host.endsWith('bsky.app') ||
host.endsWith('bsky.social') ||
host.endsWith('blueskyweb.xyz')
) {
// if this is a link to internal content, // if this is a link to internal content,
// warn if it represents itself as a URL to another app // warn if it represents itself as a URL to another app
return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain) return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain)
@ -214,3 +237,8 @@ export function splitApexDomain(hostname: string): [string, string] {
hostnamep.domain, hostnamep.domain,
] ]
} }
export function createBskyAppAbsoluteUrl(path: string): string {
const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '')
return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}`
}

View File

@ -0,0 +1,2 @@
// @ts-ignore Web-only. On RN, this is set by Metro.
window.__BUNDLE_START_TIME__ = performance.now()

View File

@ -12,7 +12,7 @@ export const router = new Router({
ModerationModlists: '/moderation/modlists', ModerationModlists: '/moderation/modlists',
ModerationMutedAccounts: '/moderation/muted-accounts', ModerationMutedAccounts: '/moderation/muted-accounts',
ModerationBlockedAccounts: '/moderation/blocked-accounts', ModerationBlockedAccounts: '/moderation/blocked-accounts',
Profile: '/profile/:name', Profile: ['/profile/:name', '/profile/:name/rss'],
ProfileFollowers: '/profile/:name/followers', ProfileFollowers: '/profile/:name/followers',
ProfileFollows: '/profile/:name/follows', ProfileFollows: '/profile/:name/follows',
ProfileList: '/profile/:name/lists/:rkey', ProfileList: '/profile/:name/lists/:rkey',

View File

@ -1,6 +1,5 @@
import React from 'react' import React from 'react'
import {ListRenderItemInfo, Pressable} from 'react-native' import {ListRenderItemInfo, Pressable} from 'react-native'
import {atoms as a, useBreakpoints} from '#/alf'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {useSetMinimalShellMode} from 'state/shell' import {useSetMinimalShellMode} from 'state/shell'
import {ViewHeader} from 'view/com/util/ViewHeader' import {ViewHeader} from 'view/com/util/ViewHeader'
@ -19,11 +18,11 @@ import {List} from 'view/com/util/List'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles' import {sanitizeHandle} from 'lib/strings/handles'
import {CenteredView} from 'view/com/util/Views'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox' import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {HITSLOP_10} from 'lib/constants' import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection' import {isNative} from 'platform/detection'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const renderItem = ({item}: ListRenderItemInfo<PostView>) => { const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
return <Post post={item} /> return <Post post={item} />
@ -38,12 +37,12 @@ export default function HashtagScreen({
}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) { }: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
const {tag, author} = route.params const {tag, author} = route.params
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {gtMobile} = useBreakpoints()
const {_} = useLingui() const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTR, setIsPTR] = React.useState(false) const [isPTR, setIsPTR] = React.useState(false)
const fullTag = React.useMemo(() => { const fullTag = React.useMemo(() => {
return `#${tag.replaceAll('%23', '#')}` return `#${decodeURIComponent(tag)}`
}, [tag]) }, [tag])
const queryParam = React.useMemo(() => { const queryParam = React.useMemo(() => {
@ -84,7 +83,7 @@ export default function HashtagScreen({
const onShare = React.useCallback(() => { const onShare = React.useCallback(() => {
const url = new URL('https://bsky.app') const url = new URL('https://bsky.app')
url.pathname = `/hashtag/${tag}` url.pathname = `/hashtag/${decodeURIComponent(tag)}`
if (author) { if (author) {
url.searchParams.set('author', author) url.searchParams.set('author', author)
} }
@ -103,7 +102,7 @@ export default function HashtagScreen({
}, [isFetching, hasNextPage, error, fetchNextPage]) }, [isFetching, hasNextPage, error, fetchNextPage])
return ( return (
<CenteredView style={a.flex_1} sideBorders={gtMobile}> <>
<ViewHeader <ViewHeader
title={headerTitle} title={headerTitle}
subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined} subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
@ -157,8 +156,10 @@ export default function HashtagScreen({
onRetry={fetchNextPage} onRetry={fetchNextPage}
/> />
} }
initialNumToRender={initialNumToRender}
windowSize={11}
/> />
)} )}
</CenteredView> </>
) )
} }

View File

@ -1,8 +1,9 @@
import React from 'react' import React from 'react'
import {SharedValue, useSharedValue} from 'react-native-reanimated'
import {DialogControlRefProps} from '#/components/Dialog' import {DialogControlRefProps} from '#/components/Dialog'
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context' import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
const DialogContext = React.createContext<{ interface IDialogContext {
/** /**
* The currently active `useDialogControl` hooks. * The currently active `useDialogControl` hooks.
*/ */
@ -14,19 +15,24 @@ const DialogContext = React.createContext<{
* `useId`. * `useId`.
*/ */
openDialogs: React.MutableRefObject<Set<string>> openDialogs: React.MutableRefObject<Set<string>>
}>({ /**
activeDialogs: { * The counterpart to `accessibilityViewIsModal` for Android. This property
current: new Map(), * applies to the parent of all non-modal views, and prevents TalkBack from
}, * navigating within content beneath an open dialog.
openDialogs: { *
current: new Set(), * @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
}, */
}) importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
}
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
const DialogControlContext = React.createContext<{ const DialogControlContext = React.createContext<{
closeAllDialogs(): boolean closeAllDialogs(): boolean
setDialogIsOpen(id: string, isOpen: boolean): void
}>({ }>({
closeAllDialogs: () => false, closeAllDialogs: () => false,
setDialogIsOpen: () => {},
}) })
export function useDialogStateContext() { export function useDialogStateContext() {
@ -42,14 +48,46 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
Map<string, React.MutableRefObject<DialogControlRefProps>> Map<string, React.MutableRefObject<DialogControlRefProps>>
>(new Map()) >(new Map())
const openDialogs = React.useRef<Set<string>>(new Set()) const openDialogs = React.useRef<Set<string>>(new Set())
const importantForAccessibility = useSharedValue<
'auto' | 'no-hide-descendants'
>('auto')
const closeAllDialogs = React.useCallback(() => { const closeAllDialogs = React.useCallback(() => {
activeDialogs.current.forEach(dialog => dialog.current.close()) activeDialogs.current.forEach(dialog => dialog.current.close())
return openDialogs.current.size > 0 return openDialogs.current.size > 0
}, []) }, [])
const context = React.useMemo(() => ({activeDialogs, openDialogs}), []) const setDialogIsOpen = React.useCallback(
const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs]) (id: string, isOpen: boolean) => {
if (isOpen) {
openDialogs.current.add(id)
importantForAccessibility.value = 'no-hide-descendants'
} else {
openDialogs.current.delete(id)
if (openDialogs.current.size < 1) {
importantForAccessibility.value = 'auto'
}
}
},
[importantForAccessibility],
)
const context = React.useMemo<IDialogContext>(
() => ({
activeDialogs: {
current: new Map(),
},
openDialogs: {
current: new Set(),
},
importantForAccessibility,
}),
[importantForAccessibility],
)
const controls = React.useMemo(
() => ({closeAllDialogs, setDialogIsOpen}),
[closeAllDialogs, setDialogIsOpen],
)
return ( return (
<DialogContext.Provider value={context}> <DialogContext.Provider value={context}>

View File

@ -5,6 +5,11 @@ import * as WebBrowser from 'expo-web-browser'
import {isNative} from '#/platform/detection' import {isNative} from '#/platform/detection'
import {useModalControls} from '../modals' import {useModalControls} from '../modals'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {
isBskyRSSUrl,
isRelativeUrl,
createBskyAppAbsoluteUrl,
} from 'lib/strings/url-helpers'
type StateContext = persisted.Schema['useInAppBrowser'] type StateContext = persisted.Schema['useInAppBrowser']
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
@ -57,6 +62,10 @@ export function useOpenLink() {
const openLink = React.useCallback( const openLink = React.useCallback(
(url: string, override?: boolean) => { (url: string, override?: boolean) => {
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
url = createBskyAppAbsoluteUrl(url)
}
if (isNative && !url.startsWith('mailto:')) { if (isNative && !url.startsWith('mailto:')) {
if (override === undefined && enabled === undefined) { if (override === undefined && enabled === undefined) {
openModal({ openModal({

View File

@ -7,7 +7,7 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] = export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
{ {
hideReplies: false, hideReplies: false,
hideRepliesByUnfollowed: false, hideRepliesByUnfollowed: true,
hideRepliesByLikeCount: 0, hideRepliesByLikeCount: 0,
hideReposts: false, hideReposts: false,
hideQuotePosts: false, hideQuotePosts: false,

View File

@ -169,7 +169,7 @@ export function usePreferencesSetBirthDateMutation() {
return useMutation<void, unknown, {birthDate: Date}>({ return useMutation<void, unknown, {birthDate: Date}>({
mutationFn: async ({birthDate}: {birthDate: Date}) => { mutationFn: async ({birthDate}: {birthDate: Date}) => {
await getAgent().setPersonalDetails({birthDate}) await getAgent().setPersonalDetails({birthDate: birthDate.toISOString()})
// triggers a refetch // triggers a refetch
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: preferencesQueryKey, queryKey: preferencesQueryKey,

View File

@ -4,7 +4,6 @@ import {
Keyboard, Keyboard,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
TouchableWithoutFeedback,
View, View,
} from 'react-native' } from 'react-native'
import {CreateAccountState, CreateAccountDispatch, is18} from './state' import {CreateAccountState, CreateAccountDispatch, is18} from './state'
@ -19,7 +18,6 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
@ -49,7 +47,6 @@ export function Step1({
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {openModal} = useModalControls()
const serverInputControl = useDialogControl() const serverInputControl = useDialogControl()
const onPressSelectService = React.useCallback(() => { const onPressSelectService = React.useCallback(() => {
@ -57,10 +54,6 @@ export function Step1({
Keyboard.dismiss() Keyboard.dismiss()
}, [serverInputControl]) }, [serverInputControl])
const onPressWaitlist = React.useCallback(() => {
openModal({name: 'waitlist'})
}, [openModal])
const birthDate = React.useMemo(() => { const birthDate = React.useMemo(() => {
return sanitizeDate(uiState.birthDate) return sanitizeDate(uiState.birthDate)
}, [uiState.birthDate]) }, [uiState.birthDate])
@ -164,23 +157,7 @@ export function Step1({
</View> </View>
)} )}
{!uiState.inviteCode && uiState.isInviteCodeRequired ? ( {!uiState.isInviteCodeRequired || uiState.inviteCode ? (
<View style={[s.flexRow, s.alignCenter]}>
<Text style={pal.text}>
<Trans>Don't have an invite code?</Trans>{' '}
</Text>
<TouchableWithoutFeedback
onPress={onPressWaitlist}
accessibilityLabel={_(msg`Join the waitlist.`)}
accessibilityHint="">
<View style={styles.touchable}>
<Text style={pal.link}>
<Trans>Join the waitlist.</Trans>
</Text>
</View>
</TouchableWithoutFeedback>
</View>
) : (
<> <>
<View style={s.pb20}> <View style={s.pb20}>
<Text <Text
@ -260,7 +237,7 @@ export function Step1({
/> />
)} )}
</> </>
)} ) : undefined}
</> </>
)} )}
</View> </View>

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import Animated from 'react-native-reanimated'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile' import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
@ -12,6 +13,8 @@ import {
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {CogIcon} from '#/lib/icons' import {CogIcon} from '#/lib/icons'
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
import {useShellLayout} from '#/state/shell/shell-layout'
export function HomeHeaderLayout(props: { export function HomeHeaderLayout(props: {
children: React.ReactNode children: React.ReactNode
@ -33,6 +36,8 @@ function HomeHeaderLayoutDesktopAndTablet({
tabBarAnchor: JSX.Element | null | undefined tabBarAnchor: JSX.Element | null | undefined
}) { }) {
const pal = usePalette('default') const pal = usePalette('default')
const {headerMinimalShellTransform} = useMinimalShellMode()
const {headerHeight} = useShellLayout()
const {_} = useLingui() const {_} = useLingui()
return ( return (
@ -60,9 +65,19 @@ function HomeHeaderLayoutDesktopAndTablet({
</Link> </Link>
</View> </View>
{tabBarAnchor} {tabBarAnchor}
<View style={[pal.view, pal.border, styles.bar, styles.tabBar]}> <Animated.View
onLayout={e => {
headerHeight.value = e.nativeEvent.layout.height
}}
style={[
pal.view,
pal.border,
styles.bar,
styles.tabBar,
headerMinimalShellTransform,
]}>
{children} {children}
</View> </Animated.View>
</> </>
) )
} }

View File

@ -20,7 +20,6 @@ import * as ReportModal from './report/Modal'
import * as AppealLabelModal from './AppealLabel' import * as AppealLabelModal from './AppealLabel'
import * as DeleteAccountModal from './DeleteAccount' import * as DeleteAccountModal from './DeleteAccount'
import * as ChangeHandleModal from './ChangeHandle' import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes' import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords' import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
@ -109,9 +108,6 @@ export function ModalsContainer() {
} else if (activeModal?.name === 'change-handle') { } else if (activeModal?.name === 'change-handle') {
snapPoints = ChangeHandleModal.snapPoints snapPoints = ChangeHandleModal.snapPoints
element = <ChangeHandleModal.Component {...activeModal} /> element = <ChangeHandleModal.Component {...activeModal} />
} else if (activeModal?.name === 'waitlist') {
snapPoints = WaitlistModal.snapPoints
element = <WaitlistModal.Component />
} else if (activeModal?.name === 'invite-codes') { } else if (activeModal?.name === 'invite-codes') {
snapPoints = InviteCodesModal.snapPoints snapPoints = InviteCodesModal.snapPoints
element = <InviteCodesModal.Component /> element = <InviteCodesModal.Component />

View File

@ -22,7 +22,6 @@ import * as CropImageModal from './crop-image/CropImage.web'
import * as AltTextImageModal from './AltImage' import * as AltTextImageModal from './AltImage'
import * as EditImageModal from './EditImage' import * as EditImageModal from './EditImage'
import * as ChangeHandleModal from './ChangeHandle' import * as ChangeHandleModal from './ChangeHandle'
import * as WaitlistModal from './Waitlist'
import * as InviteCodesModal from './InviteCodes' import * as InviteCodesModal from './InviteCodes'
import * as AddAppPassword from './AddAppPasswords' import * as AddAppPassword from './AddAppPasswords'
import * as ContentFilteringSettingsModal from './ContentFilteringSettings' import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
element = <ThreadgateModal.Component {...modal} /> element = <ThreadgateModal.Component {...modal} />
} else if (modal.name === 'change-handle') { } else if (modal.name === 'change-handle') {
element = <ChangeHandleModal.Component {...modal} /> element = <ChangeHandleModal.Component {...modal} />
} else if (modal.name === 'waitlist') {
element = <WaitlistModal.Component />
} else if (modal.name === 'invite-codes') { } else if (modal.name === 'invite-codes') {
element = <InviteCodesModal.Component /> element = <InviteCodesModal.Component />
} else if (modal.name === 'add-app-password') { } else if (modal.name === 'add-app-password') {

View File

@ -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<string>('')
const [isEmailSent, setIsEmailSent] = React.useState<boolean>(false)
const [isProcessing, setIsProcessing] = React.useState<boolean>(false)
const [error, setError] = React.useState<string>('')
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 (
<View style={[styles.container, pal.view]}>
<View style={[styles.innerContainer, pal.view]}>
<Text type="title-xl" style={[styles.title, pal.text]}>
<Trans>Join the waitlist</Trans>
</Text>
<Text type="lg" style={[styles.description, pal.text]}>
<Trans>
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.
</Trans>
</Text>
<TextInput
style={[styles.textInput, pal.borderDark, pal.text, s.mb10, s.mt10]}
placeholder={_(msg`Enter your email`)}
placeholderTextColor={pal.textLight.color}
autoCapitalize="none"
autoCorrect={false}
keyboardAppearance={theme.colorScheme}
value={email}
onChangeText={setEmail}
onSubmitEditing={onPressSignup}
enterKeyHint="done"
accessible={true}
accessibilityLabel={_(msg`Email`)}
accessibilityHint={_(
msg`Input your email to get on the Bluesky waitlist`,
)}
/>
{error ? (
<View style={s.mt10}>
<ErrorMessage message={error} style={styles.error} />
</View>
) : undefined}
{isProcessing ? (
<View style={[styles.btn, s.mt10]}>
<ActivityIndicator />
</View>
) : isEmailSent ? (
<View style={[styles.btn, s.mt10]}>
<FontAwesomeIcon
icon="check"
style={pal.text as FontAwesomeIconStyle}
/>
<Text style={[s.ml10, pal.text]}>
<Trans>
Your email has been saved! We&apos;ll be in touch soon.
</Trans>
</Text>
</View>
) : (
<>
<TouchableOpacity
onPress={onPressSignup}
accessibilityRole="button"
accessibilityHint={_(
msg`Confirms signing up ${email} to the waitlist`,
)}>
<LinearGradient
colors={[gradients.blueLight.start, gradients.blueLight.end]}
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={[styles.btn]}>
<Text type="button-lg" style={[s.white, s.bold]}>
<Trans>Join Waitlist</Trans>
</Text>
</LinearGradient>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, s.mt10]}
onPress={onCancel}
accessibilityRole="button"
accessibilityLabel={_(msg`Cancel waitlist signup`)}
accessibilityHint={_(
msg`Exits signing up for waitlist with ${email}`,
)}
onAccessibilityEscape={onCancel}>
<Text type="button-lg" style={pal.textLight}>
<Trans>Cancel</Trans>
</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
)
}
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,
},
})

View File

@ -228,6 +228,7 @@ let FeedItem = ({
text={sanitizeDisplayName( text={sanitizeDisplayName(
authors[0].displayName || authors[0].handle, authors[0].displayName || authors[0].handle,
)} )}
disableMismatchWarning
/> />
{authors.length > 1 ? ( {authors.length > 1 ? (
<> <>

View File

@ -32,6 +32,7 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader' import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home' import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
@ -84,6 +85,7 @@ let Feed = ({
const {_} = useLingui() const {_} = useLingui()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null) const checkForNewRef = React.useRef<(() => void) | null>(null)
const lastFetchRef = React.useRef<number>(Date.now()) const lastFetchRef = React.useRef<number>(Date.now())
@ -327,6 +329,8 @@ let Feed = ({
desktopFixedHeight={ desktopFixedHeight={
desktopFixedHeightOffset ? desktopFixedHeightOffset : true desktopFixedHeightOffset ? desktopFixedHeightOffset : true
} }
initialNumToRender={initialNumToRender}
windowSize={11}
/> />
</View> </View>
) )

View File

@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native'
export function EventStopper({ export function EventStopper({
children, children,
style, 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) => { const stop = (e: any) => {
e.stopPropagation() e.stopPropagation()
} }
@ -18,7 +25,7 @@ export function EventStopper({
onTouchEnd={stop} onTouchEnd={stop}
// @ts-ignore web only -prf // @ts-ignore web only -prf
onClick={stop} onClick={stop}
onKeyDown={stop} onKeyDown={onKeyDown ? stop : undefined}
style={style}> style={style}>
{children} {children}
</View> </View>

View File

@ -5,4 +5,6 @@ export function CenteredView({
style, style,
sideBorders, sideBorders,
...props ...props
}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) }: React.PropsWithChildren<
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
>)

View File

@ -32,8 +32,11 @@ interface AddedProps {
export function CenteredView({ export function CenteredView({
style, style,
sideBorders, sideBorders,
topBorder,
...props ...props
}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) { }: React.PropsWithChildren<
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
>) {
const pal = usePalette('default') const pal = usePalette('default')
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
if (!isMobile) { if (!isMobile) {
@ -46,6 +49,12 @@ export function CenteredView({
}) })
style = addStyle(style, pal.border) style = addStyle(style, pal.border)
} }
if (topBorder) {
style = addStyle(style, {
borderTopWidth: 1,
})
style = addStyle(style, pal.border)
}
return <View style={style} {...props} /> return <View style={style} {...props} />
} }

View File

@ -1,8 +1,5 @@
import React, {useState, useCallback} from 'react' import React, {useState, useCallback} from 'react'
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
import DateTimePicker, {
DateTimePickerEvent,
} from '@react-native-community/datetimepicker'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {getLocales} from 'expo-localization' import {getLocales} from 'expo-localization'
import DatePicker from 'react-native-date-picker'
const LOCALE = getLocales()[0] const LOCALE = getLocales()[0]
@ -43,11 +41,9 @@ export function DateInput(props: Props) {
}, [props.handleAsUTC]) }, [props.handleAsUTC])
const onChangeInternal = useCallback( const onChangeInternal = useCallback(
(event: DateTimePickerEvent, date: Date | undefined) => { (date: Date) => {
setShow(false) setShow(false)
if (date) { props.onChange(date)
props.onChange(date)
}
}, },
[setShow, props], [setShow, props],
) )
@ -56,6 +52,10 @@ export function DateInput(props: Props) {
setShow(true) setShow(true)
}, [setShow]) }, [setShow])
const onCancel = useCallback(() => {
setShow(false)
}, [])
return ( return (
<View> <View>
{isAndroid && ( {isAndroid && (
@ -80,15 +80,17 @@ export function DateInput(props: Props) {
</Button> </Button>
)} )}
{(isIOS || show) && ( {(isIOS || show) && (
<DateTimePicker <DatePicker
testID={props.testID ? `${props.testID}-datepicker` : undefined} timeZoneOffsetInMinutes={0}
modal={isAndroid}
open={isAndroid}
theme={theme.colorScheme}
date={props.value}
onDateChange={onChangeInternal}
onConfirm={onChangeInternal}
onCancel={onCancel}
mode="date" mode="date"
timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined} testID={props.testID ? `${props.testID}-datepicker` : undefined}
display="spinner"
// @ts-ignore applies in iOS only -prf
themeVariant={theme.colorScheme}
value={props.value}
onChange={onChangeInternal}
accessibilityLabel={props.accessibilityLabel} accessibilityLabel={props.accessibilityLabel}
accessibilityHint={props.accessibilityHint} accessibilityHint={props.accessibilityHint}
accessibilityLabelledBy={props.accessibilityLabelledBy} accessibilityLabelledBy={props.accessibilityLabelledBy}

View File

@ -1,5 +1,11 @@
import React, {memo} from 'react' 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 Clipboard from '@react-native-clipboard/clipboard'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
@ -12,10 +18,6 @@ import {
import {toShareUrl} from 'lib/strings/url-helpers' import {toShareUrl} from 'lib/strings/url-helpers'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {shareUrl} from 'lib/sharing' import {shareUrl} from 'lib/sharing'
import {
NativeDropdown,
DropdownItem as NativeDropdownItem,
} from './NativeDropdown'
import * as Toast from '../Toast' import * as Toast from '../Toast'
import {EventStopper} from '../EventStopper' import {EventStopper} from '../EventStopper'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
@ -36,6 +38,19 @@ import {isWeb} from '#/platform/detection'
import {richTextToString} from '#/lib/strings/rich-text-helpers' import {richTextToString} from '#/lib/strings/rich-text-helpers'
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' 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 = ({ let PostDropdownBtn = ({
testID, testID,
postAuthor, postAuthor,
@ -45,6 +60,7 @@ let PostDropdownBtn = ({
richText, richText,
style, style,
showAppealLabelItem, showAppealLabelItem,
hitSlop,
}: { }: {
testID: string testID: string
postAuthor: AppBskyActorDefs.ProfileViewBasic postAuthor: AppBskyActorDefs.ProfileViewBasic
@ -54,9 +70,11 @@ let PostDropdownBtn = ({
richText: RichTextAPI richText: RichTextAPI
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
showAppealLabelItem?: boolean showAppealLabelItem?: boolean
hitSlop?: PressableProps['hitSlop']
}): React.ReactNode => { }): React.ReactNode => {
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const theme = useTheme() const theme = useTheme()
const alf = useAlf()
const {_} = useLingui() const {_} = useLingui()
const defaultCtrlColor = theme.palette.default.postCtrl const defaultCtrlColor = theme.palette.default.postCtrl
const {openModal} = useModalControls() const {openModal} = useModalControls()
@ -151,173 +169,189 @@ let PostDropdownBtn = ({
hidePost({uri: postUri}) hidePost({uri: postUri})
}, [postUri, hidePost]) }, [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 ( return (
<EventStopper> <EventStopper onKeyDown={false}>
<NativeDropdown <Menu.Root>
testID={testID} <Menu.Trigger label={_(msg`Open post options menu`)}>
items={dropdownItems} {({props, state}) => {
accessibilityLabel={_(msg`More post options`)} const styles = [
accessibilityHint=""> style,
<View style={style}> a.rounded_full,
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} /> (state.hovered || state.focused || state.pressed) && [
</View> web({outline: 0}),
</NativeDropdown> alf.atoms.bg_contrast_25,
],
]
return isWeb ? (
<View {...props} testID={testID} style={styles}>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={defaultCtrlColor}
style={{pointerEvents: 'none'}}
/>
</View>
) : (
<Pressable
{...props}
hitSlop={hitSlop}
testID={testID}
style={styles}>
<FontAwesomeIcon
icon="ellipsis"
size={20}
color={defaultCtrlColor}
style={{pointerEvents: 'none'}}
/>
</Pressable>
)
}}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item
testID="postDropdownTranslateBtn"
label={_(msg`Translate`)}
onPress={onOpenTranslate}>
<Menu.ItemText>{_(msg`Translate`)}</Menu.ItemText>
<Menu.ItemIcon icon={Translate} position="right" />
</Menu.Item>
<Menu.Item
testID="postDropdownCopyTextBtn"
label={_(msg`Copy post text`)}
onPress={onCopyPostText}>
<Menu.ItemText>{_(msg`Copy post text`)}</Menu.ItemText>
<Menu.ItemIcon icon={ClipboardIcon} position="right" />
</Menu.Item>
<Menu.Item
testID="postDropdownShareBtn"
label={isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
onPress={() => {
const url = toShareUrl(href)
shareUrl(url)
}}>
<Menu.ItemText>
{isWeb ? _(msg`Copy link to post`) : _(msg`Share`)}
</Menu.ItemText>
<Menu.ItemIcon icon={Share} position="right" />
</Menu.Item>
</Menu.Group>
{hasSession && (
<>
<Menu.Divider />
<Menu.Group>
<Menu.Item
testID="postDropdownMuteThreadBtn"
label={
isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`)
}
onPress={onToggleThreadMute}>
<Menu.ItemText>
{isThreadMuted
? _(msg`Unmute thread`)
: _(msg`Mute thread`)}
</Menu.ItemText>
<Menu.ItemIcon
icon={isThreadMuted ? Unmute : Mute}
position="right"
/>
</Menu.Item>
<Menu.Item
testID="postDropdownMuteWordsBtn"
label={_(msg`Mute words & tags`)}
onPress={() => mutedWordsDialogControl.open()}>
<Menu.ItemText>{_(msg`Mute words & tags`)}</Menu.ItemText>
<Menu.ItemIcon icon={Filter} position="right" />
</Menu.Item>
{!isAuthor && !isPostHidden && (
<Menu.Item
testID="postDropdownHideBtn"
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,
})
}}>
<Menu.ItemText>{_(msg`Hide post`)}</Menu.ItemText>
<Menu.ItemIcon icon={EyeSlash} position="right" />
</Menu.Item>
)}
</Menu.Group>
</>
)}
<Menu.Divider />
<Menu.Group>
{!isAuthor && (
<Menu.Item
testID="postDropdownReportBtn"
label={_(msg`Report post`)}
onPress={() => {
openModal({
name: 'report',
uri: postUri,
cid: postCid,
})
}}>
<Menu.ItemText>{_(msg`Report post`)}</Menu.ItemText>
<Menu.ItemIcon icon={Warning} position="right" />
</Menu.Item>
)}
{isAuthor && (
<Menu.Item
testID="postDropdownDeleteBtn"
label={_(msg`Delete post`)}
onPress={() => {
openModal({
name: 'confirm',
title: _(msg`Delete this post?`),
message: _(msg`Are you sure? This cannot be undone.`),
onPressConfirm: onDeletePost,
})
}}>
<Menu.ItemText>{_(msg`Delete post`)}</Menu.ItemText>
<Menu.ItemIcon icon={Trash} position="right" />
</Menu.Item>
)}
{showAppealLabelItem && (
<>
<Menu.Divider />
<Menu.Item
testID="postDropdownAppealBtn"
label={_(msg`Appeal content warning`)}
onPress={() => {
openModal({
name: 'appeal-label',
uri: postUri,
cid: postCid,
})
}}>
<Menu.ItemText>
{_(msg`Appeal content warning`)}
</Menu.ItemText>
<Menu.ItemIcon icon={CircleInfo} position="right" />
</Menu.Item>
</>
)}
</Menu.Group>
</Menu.Outer>
</Menu.Root>
</EventStopper> </EventStopper>
) )
} }

View File

@ -57,6 +57,7 @@ const styles = StyleSheet.create({
btn: { btn: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
flexGrow: 1,
borderWidth: 1, borderWidth: 1,
borderLeftWidth: 0, borderLeftWidth: 0,
paddingHorizontal: 10, paddingHorizontal: 10,

View File

@ -212,9 +212,7 @@ let PostCtrls = ({
style={[styles.btn]} style={[styles.btn]}
onPress={onShare} onPress={onShare}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={`${ accessibilityLabel={`${_(msg`Share`)}`}
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
accessibilityHint="" accessibilityHint=""
hitSlop={big ? HITSLOP_20 : HITSLOP_10}> hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} /> <ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
@ -231,6 +229,7 @@ let PostCtrls = ({
richText={richText} richText={richText}
showAppealLabelItem={showAppealLabelItem} showAppealLabelItem={showAppealLabelItem}
style={styles.btnPad} style={styles.btnPad}
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
/> />
</View> </View>
</View> </View>

View File

@ -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 (
<View style={[a.gap_md]}>
<View style={[a.flex_row, a.align_start]}>
<Menu.Root control={menuControl}>
<Menu.Trigger label="Open basic menu" style={[a.flex_1]}>
{({state, props}) => {
return (
<Text
{...props}
style={[
a.py_sm,
a.px_md,
a.rounded_sm,
t.atoms.bg_contrast_50,
(state.hovered || state.focused || state.pressed) && [
t.atoms.bg_contrast_200,
],
]}>
Open
</Text>
)
}}
</Menu.Trigger>
<Menu.Outer>
<Menu.Group>
<Menu.Item label="Click me" onPress={() => {}}>
<Menu.ItemIcon icon={Search} />
<Menu.ItemText>Click me</Menu.ItemText>
</Menu.Item>
<Menu.Item
label="Another item"
onPress={() => menuControl.close()}>
<Menu.ItemText>Another item</Menu.ItemText>
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Group>
<Menu.Item label="Click me" onPress={() => {}}>
<Menu.ItemIcon icon={Search} />
<Menu.ItemText>Click me</Menu.ItemText>
</Menu.Item>
<Menu.Item
label="Another item"
onPress={() => menuControl.close()}>
<Menu.ItemText>Another item</Menu.ItemText>
</Menu.Item>
</Menu.Group>
<Menu.Divider />
<Menu.Item label="Click me" onPress={() => {}}>
<Menu.ItemIcon icon={Search} />
<Menu.ItemText>Click me</Menu.ItemText>
</Menu.Item>
</Menu.Outer>
</Menu.Root>
</View>
</View>
)
}

View File

@ -16,6 +16,7 @@ import {Dialogs} from './Dialogs'
import {Breakpoints} from './Breakpoints' import {Breakpoints} from './Breakpoints'
import {Shadows} from './Shadows' import {Shadows} from './Shadows'
import {Icons} from './Icons' import {Icons} from './Icons'
import {Menus} from './Menus'
export function Storybook() { export function Storybook() {
const t = useTheme() const t = useTheme()
@ -84,6 +85,7 @@ export function Storybook() {
<Links /> <Links />
<Forms /> <Forms />
<Dialogs /> <Dialogs />
<Menus />
<Breakpoints /> <Breakpoints />
</View> </View>
</CenteredView> </CenteredView>

View File

@ -391,7 +391,7 @@ export function DesktopLeftNav() {
<FontAwesomeIcon <FontAwesomeIcon
icon="hand" icon="hand"
style={pal.text as FontAwesomeIconStyle} style={pal.text as FontAwesomeIconStyle}
size={isDesktop ? 20 : 26} size={isDesktop ? 23 : 26}
/> />
} }
label={_(msg`Moderation`)} label={_(msg`Moderation`)}

View File

@ -30,6 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util'
import * as notifications from 'lib/notifications/notifications' import * as notifications from 'lib/notifications/notifications'
import {Outlet as PortalOutlet} from '#/components/Portal' import {Outlet as PortalOutlet} from '#/components/Portal'
import {MutedWordsDialog} from '#/components/dialogs/MutedWords' import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
import {useDialogStateContext} from 'state/dialogs'
import Animated from 'react-native-reanimated'
function ShellInner() { function ShellInner() {
const isDrawerOpen = useIsDrawerOpen() const isDrawerOpen = useIsDrawerOpen()
@ -53,6 +55,7 @@ function ShellInner() {
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state)) const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const closeAnyActiveElement = useCloseAnyActiveElement() const closeAnyActiveElement = useCloseAnyActiveElement()
const {importantForAccessibility} = useDialogStateContext()
// start undefined // start undefined
const currentAccountDid = React.useRef<string | undefined>(undefined) const currentAccountDid = React.useRef<string | undefined>(undefined)
@ -80,7 +83,9 @@ function ShellInner() {
return ( return (
<> <>
<View style={containerPadding}> <Animated.View
style={containerPadding}
importantForAccessibility={importantForAccessibility}>
<ErrorBoundary> <ErrorBoundary>
<Drawer <Drawer
renderDrawerContent={renderDrawerContent} renderDrawerContent={renderDrawerContent}
@ -92,7 +97,7 @@ function ShellInner() {
<TabsNavigator /> <TabsNavigator />
</Drawer> </Drawer>
</ErrorBoundary> </ErrorBoundary>
</View> </Animated.View>
<Composer winHeight={winDim.height} /> <Composer winHeight={winDim.height} />
<ModalsContainer /> <ModalsContainer />
<MutedWordsDialog /> <MutedWordsDialog />

View File

@ -47,6 +47,9 @@
height: calc(100% + env(safe-area-inset-top)); height: calc(100% + env(safe-area-inset-top));
scrollbar-gutter: stable both-edges; 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 */ /* Buttons and inputs have a font set by UA, so we'll have to reset that */
button, input, textarea { button, input, textarea {
@ -217,6 +220,7 @@
} }
/* NativeDropdown component */ /* NativeDropdown component */
.radix-dropdown-item:focus,
.nativeDropdown-item:focus { .nativeDropdown-item:focus {
outline: none; outline: none;
} }

275
yarn.lock
View File

@ -34,10 +34,10 @@
jsonpointer "^5.0.0" jsonpointer "^5.0.0"
leven "^3.1.0" leven "^3.1.0"
"@atproto/api@^0.10.4": "@atproto/api@^0.10.5":
version "0.10.4" version "0.10.5"
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.4.tgz#b73446f2344783c42c6040082756449443f15750" resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.5.tgz#e778e2843d08690df8df81f24028a7578e9b3cb4"
integrity sha512-9gwZt4v4pngfD4mgsET9i9Ym0PpMSzftTzqBjCbFpObx15zMkFemYnLUnyT/NEww2u/aRxjAe2TeBnU0dIbbuQ== integrity sha512-GYdST5sPKU2JnPmm8x3KqjOSlDiYXrp4GkW7bpQTVLPabnUNq5NLN6HJEoJABjjOAsaLF12rBoV+JpRb1UjNsQ==
dependencies: dependencies:
"@atproto/common-web" "^0.2.3" "@atproto/common-web" "^0.2.3"
"@atproto/lexicon" "^0.3.2" "@atproto/lexicon" "^0.3.2"
@ -3115,6 +3115,27 @@
xcode "^3.0.1" xcode "^3.0.1"
xml2js "0.6.0" 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": "@expo/config-plugins@~7.8.2":
version "7.8.2" version "7.8.2"
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.2.tgz#c00ce93c4d6c2cb9e345ed9cd56ceeea05ab8ddb" resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.2.tgz#c00ce93c4d6c2cb9e345ed9cd56ceeea05ab8ddb"
@ -3138,6 +3159,11 @@
xcode "^3.0.1" xcode "^3.0.1"
xml2js "0.6.0" 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": "@expo/config-types@^50.0.0", "@expo/config-types@^50.0.0-alpha.1":
version "50.0.0" version "50.0.0"
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b"
@ -3160,6 +3186,23 @@
slugify "^1.3.4" slugify "^1.3.4"
sucrase "^3.20.0" 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": "@expo/config@~8.5.0":
version "8.5.0" version "8.5.0"
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.0.tgz#c618e016c3272335e33fec02fb7fd67e4dcc3342" resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.0.tgz#c618e016c3272335e33fec02fb7fd67e4dcc3342"
@ -3259,6 +3302,15 @@
semver "7.3.2" semver "7.3.2"
tempy "0.3.0" 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": "@expo/json-file@^8.2.37":
version "8.2.37" version "8.2.37"
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.37.tgz#9c02d3b42134907c69cc0a027b18671b69344049" resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.37.tgz#9c02d3b42134907c69cc0a027b18671b69344049"
@ -3328,6 +3380,15 @@
split "^1.0.1" split "^1.0.1"
sudo-prompt "9.1.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": "@expo/plist@^0.1.0":
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.0.tgz#eabc95f951d14e10c87fd0443ee01d567371f058" resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.0.tgz#eabc95f951d14e10c87fd0443ee01d567371f058"
@ -4467,6 +4528,18 @@
"@radix-ui/react-use-callback-ref" "1.0.1" "@radix-ui/react-use-callback-ref" "1.0.1"
"@radix-ui/react-use-escape-keydown" "1.0.3" "@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": "@radix-ui/react-dropdown-menu@^2.0.1":
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170" resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170"
@ -4481,6 +4554,20 @@
"@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-controllable-state" "1.0.1" "@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": "@radix-ui/react-focus-guards@1.0.1":
version "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" resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
@ -4498,6 +4585,16 @@
"@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-use-callback-ref" "1.0.1" "@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": "@radix-ui/react-id@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
@ -4531,6 +4628,31 @@
aria-hidden "^1.1.1" aria-hidden "^1.1.1"
react-remove-scroll "2.5.5" 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": "@radix-ui/react-popper@1.1.2":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9" resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
@ -4548,6 +4670,23 @@
"@radix-ui/react-use-size" "1.0.1" "@radix-ui/react-use-size" "1.0.1"
"@radix-ui/rect" "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": "@radix-ui/react-portal@1.0.3":
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
@ -4556,6 +4695,14 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@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": "@radix-ui/react-presence@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
@ -4664,6 +4811,13 @@
dependencies: dependencies:
merge-options "^3.0.4" 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": "@react-native-camera-roll/camera-roll@^5.2.2":
version "5.7.2" version "5.7.2"
resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz#db11525ae26c8a61630c424aebd323a7c784a921" resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz#db11525ae26c8a61630c424aebd323a7c784a921"
@ -4832,13 +4986,6 @@
prompts "^2.4.2" prompts "^2.4.2"
semver "^7.5.2" semver "^7.5.2"
"@react-native-community/datetimepicker@7.6.1":
version "7.6.1"
resolved "https://registry.yarnpkg.com/@react-native-community/datetimepicker/-/datetimepicker-7.6.1.tgz#98bdee01e3df490526ee1125e438c2030becac1f"
integrity sha512-g66Q2Kd9Uw3eRL7kkrTsGhi+eXxNoPDRFYH6z78sZQuYjPkUQgJDDMUYgBmaBsQx/fKMtemPrCj1ulGmyi0OSw==
dependencies:
invariant "^2.2.4"
"@react-native-community/eslint-config@^3.0.0": "@react-native-community/eslint-config@^3.0.0":
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d" resolved "https://registry.yarnpkg.com/@react-native-community/eslint-config/-/eslint-config-3.2.0.tgz#42f677d5fff385bccf1be1d3b8faa8c086cf998d"
@ -8038,7 +8185,7 @@
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99"
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
"@xmldom/xmldom@~0.7.7": "@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7":
version "0.7.13" version "0.7.13"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3"
integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g== integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==
@ -11651,6 +11798,14 @@ expo-camera@~14.0.1:
dependencies: dependencies:
invariant "^2.2.4" 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: expo-constants@~15.4.0:
version "15.4.1" version "15.4.1"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.1.tgz#f76f347cf687b6630e1e3b9a385a4e42771671a4" resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.1.tgz#f76f347cf687b6630e1e3b9a385a4e42771671a4"
@ -11700,6 +11855,13 @@ expo-dev-menu@4.5.3:
expo-dev-menu-interface "1.7.2" expo-dev-menu-interface "1.7.2"
semver "^7.5.3" 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: expo-device@~5.9.2:
version "5.9.2" version "5.9.2"
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.9.2.tgz#697e96f52d213a141b6f265f1e274e9d5e98c92c" resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.9.2.tgz#697e96f52d213a141b6f265f1e274e9d5e98c92c"
@ -14892,6 +15054,16 @@ js-queue@2.0.2:
dependencies: dependencies:
easy-stack "^1.0.1" 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: js-sha256@^0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
@ -15076,7 +15248,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" 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== integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json5@^1.0.2: json5@^1.0.1, json5@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
@ -18372,11 +18544,23 @@ react-is@^17.0.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== 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: react-native-appstate-hook@^1.0.6:
version "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" resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ== integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
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-dotenv@^3.3.1: react-native-dotenv@^3.3.1:
version "3.4.9" version "3.4.9"
resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.9.tgz#621c5b0c1d0c5c7f569bfe5a1d804bec7885c010" resolved "https://registry.yarnpkg.com/react-native-dotenv/-/react-native-dotenv-3.4.9.tgz#621c5b0c1d0c5c7f569bfe5a1d804bec7885c010"
@ -18410,6 +18594,13 @@ react-native-gesture-handler@~2.14.0:
lodash "^4.17.21" lodash "^4.17.21"
prop-types "^15.7.2" 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: react-native-get-random-values@~1.8.0:
version "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" resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16"
@ -19883,6 +20074,49 @@ standard-as-callback@^2.1.0:
resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45"
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== 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: statuses@2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
@ -20827,6 +21061,11 @@ typescript@^5.3.3:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== 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: ua-parser-js@^0.7.33:
version "0.7.35" version "0.7.35"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307"
@ -21108,7 +21347,7 @@ utils-merge@1.0.1:
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@^3.0.1: uuid@^3.0.1, uuid@^3.3.2:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
@ -21860,6 +22099,14 @@ xml-name-validator@^4.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== 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: xml2js@0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"