Merge branch 'bluesky-social:main' into main
commit
4a2251f48b
|
@ -31,7 +31,7 @@ RUN \. "$NVM_DIR/nvm.sh" && \
|
|||
nvm use $NODE_VERSION && \
|
||||
npm install --global yarn && \
|
||||
yarn && \
|
||||
yarn intl:compile && \
|
||||
yarn intl:build && \
|
||||
yarn build-web
|
||||
|
||||
# DEBUG
|
||||
|
|
|
@ -11,6 +11,17 @@ const DARK_SPLASH_CONFIG = {
|
|||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
const SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0c7cff',
|
||||
image: './assets/splash.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
const DARK_SPLASH_CONFIG_ANDROID = {
|
||||
backgroundColor: '#0f141b',
|
||||
image: './assets/splash-dark.png',
|
||||
resizeMode: 'cover',
|
||||
}
|
||||
|
||||
module.exports = function (config) {
|
||||
/**
|
||||
* App version number. Should be incremented as part of a release cycle.
|
||||
|
@ -70,8 +81,8 @@ module.exports = function (config) {
|
|||
},
|
||||
},
|
||||
androidStatusBar: {
|
||||
barStyle: 'dark-content',
|
||||
backgroundColor: '#ffffff',
|
||||
barStyle: 'light-content',
|
||||
backgroundColor: '#00000000',
|
||||
},
|
||||
android: {
|
||||
icon: './assets/icon.png',
|
||||
|
@ -101,8 +112,8 @@ module.exports = function (config) {
|
|||
},
|
||||
],
|
||||
splash: {
|
||||
...SPLASH_CONFIG,
|
||||
dark: DARK_SPLASH_CONFIG,
|
||||
...SPLASH_CONFIG_ANDROID,
|
||||
dark: DARK_SPLASH_CONFIG_ANDROID,
|
||||
},
|
||||
},
|
||||
web: {
|
||||
|
@ -121,12 +132,14 @@ module.exports = function (config) {
|
|||
{
|
||||
ios: {
|
||||
deploymentTarget: '13.4',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 34,
|
||||
targetSdkVersion: 34,
|
||||
buildToolsVersion: '34.0.0',
|
||||
kotlinVersion: '1.8.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -140,10 +153,12 @@ module.exports = function (config) {
|
|||
'expo-notifications',
|
||||
{
|
||||
icon: './assets/icon-android-notification.png',
|
||||
color: '#ffffff',
|
||||
color: '#1185fe',
|
||||
},
|
||||
],
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesWindowBackgroundPlugin.js',
|
||||
'./plugins/shareExtension/withShareExtensions.js',
|
||||
].filter(Boolean),
|
||||
extra: {
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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),
|
||||
})
|
||||
}
|
|
@ -40,18 +40,6 @@ func run(args []string) {
|
|||
// retain old PDS env var for easy transition
|
||||
EnvVars: []string{"ATP_APPVIEW_HOST", "ATP_PDS_HOST"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mailmodo-api-key",
|
||||
Usage: "Mailmodo API key",
|
||||
Required: false,
|
||||
EnvVars: []string{"MAILMODO_API_KEY"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "mailmodo-list-name",
|
||||
Usage: "Mailmodo contact list to add email addresses to",
|
||||
Required: false,
|
||||
EnvVars: []string{"MAILMODO_LIST_NAME"},
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-address",
|
||||
Usage: "Specify the local IP/port to bind to",
|
||||
|
|
|
@ -2,11 +2,9 @@ package main
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
|
@ -29,25 +27,19 @@ import (
|
|||
)
|
||||
|
||||
type Server struct {
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
mailmodo *Mailmodo
|
||||
xrpcc *xrpc.Client
|
||||
echo *echo.Echo
|
||||
httpd *http.Server
|
||||
xrpcc *xrpc.Client
|
||||
}
|
||||
|
||||
func serve(cctx *cli.Context) error {
|
||||
debug := cctx.Bool("debug")
|
||||
httpAddress := cctx.String("http-address")
|
||||
appviewHost := cctx.String("appview-host")
|
||||
mailmodoAPIKey := cctx.String("mailmodo-api-key")
|
||||
mailmodoListName := cctx.String("mailmodo-list-name")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
|
||||
// Mailmodo client.
|
||||
mailmodo := NewMailmodo(mailmodoAPIKey, mailmodoListName)
|
||||
|
||||
// create a new session (no auth)
|
||||
xrpcc := &xrpc.Client{
|
||||
Client: cliutil.NewHttpClient(),
|
||||
|
@ -77,9 +69,8 @@ func serve(cctx *cli.Context) error {
|
|||
// server
|
||||
//
|
||||
server := &Server{
|
||||
echo: e,
|
||||
mailmodo: mailmodo,
|
||||
xrpcc: xrpcc,
|
||||
echo: e,
|
||||
xrpcc: xrpcc,
|
||||
}
|
||||
|
||||
// Create the HTTP server.
|
||||
|
@ -221,9 +212,6 @@ func serve(cctx *cli.Context) error {
|
|||
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
|
||||
|
||||
// Mailmodo
|
||||
e.POST("/api/waitlist", server.apiWaitlist)
|
||||
|
||||
// Start the server.
|
||||
log.Infof("starting server address=%s", httpAddress)
|
||||
go func() {
|
||||
|
@ -398,36 +386,3 @@ func (srv *Server) WebProfile(c echo.Context) error {
|
|||
data["requestHost"] = req.Host
|
||||
return c.Render(http.StatusOK, "profile.html", data)
|
||||
}
|
||||
|
||||
func (srv *Server) apiWaitlist(c echo.Context) error {
|
||||
type jsonError struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// Read the API request.
|
||||
type apiRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
bodyReader := http.MaxBytesReader(c.Response(), c.Request().Body, 16*1024)
|
||||
payload, err := ioutil.ReadAll(bodyReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var req apiRequest
|
||||
if err := json.Unmarshal(payload, &req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, jsonError{Error: "Invalid API request"})
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
return c.JSON(http.StatusBadRequest, jsonError{Error: "Please enter a valid email address."})
|
||||
}
|
||||
|
||||
if err := srv.mailmodo.AddToList(c.Request().Context(), req.Email); err != nil {
|
||||
log.Errorf("adding email to waitlist failed: %s", err)
|
||||
return c.JSON(http.StatusBadRequest, jsonError{
|
||||
Error: "Storing email in waitlist failed. Please enter a valid email address.",
|
||||
})
|
||||
}
|
||||
return c.JSON(http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
height: calc(100% + env(safe-area-inset-top));
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
|
@ -213,6 +216,7 @@
|
|||
}
|
||||
|
||||
/* NativeDropdown component */
|
||||
.radix-dropdown-item:focus,
|
||||
.nativeDropdown-item:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import '#/platform/markBundleStartTime'
|
||||
|
||||
import '#/platform/polyfills'
|
||||
import {registerRootComponent} from 'expo'
|
||||
import {doPolyfill} from '#/lib/api/api-polyfill'
|
||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.71.0",
|
||||
"version": "1.72.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
@ -44,7 +44,7 @@
|
|||
"update-extensions": "scripts/updateExtensions.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.10.4",
|
||||
"@atproto/api": "^0.10.5",
|
||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
|
@ -58,11 +58,11 @@
|
|||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.6.4",
|
||||
"@miblanchard/react-native-slider": "^2.3.1",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@react-native-async-storage/async-storage": "1.21.0",
|
||||
"@react-native-camera-roll/camera-roll": "^5.2.2",
|
||||
"@react-native-clipboard/clipboard": "^1.10.0",
|
||||
"@react-native-community/blur": "^4.3.0",
|
||||
"@react-native-community/datetimepicker": "7.6.1",
|
||||
"@react-native-masked-view/masked-view": "0.3.0",
|
||||
"@react-native-menu/menu": "^0.8.0",
|
||||
"@react-native-picker/picker": "2.6.1",
|
||||
|
@ -148,8 +148,10 @@
|
|||
"react-avatar-editor": "^13.0.0",
|
||||
"react-circular-progressbar": "^2.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-keyed-flatten-children": "^3.0.0",
|
||||
"react-native": "0.73.2",
|
||||
"react-native-appstate-hook": "^1.0.6",
|
||||
"react-native-date-picker": "^4.4.0",
|
||||
"react-native-drawer-layout": "^4.0.0-alpha.3",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
|
@ -177,6 +179,8 @@
|
|||
"react-responsive": "^9.0.2",
|
||||
"rn-fetch-blob": "^0.12.0",
|
||||
"sentry-expo": "~7.0.1",
|
||||
"statsig-react": "^1.36.0",
|
||||
"statsig-react-native-expo": "^4.6.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"use-deep-compare": "^1.1.0",
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -43,9 +43,12 @@ import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unre
|
|||
import * as persisted from '#/state/persisted'
|
||||
import {Splash} from '#/Splash'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
import {StatusBar} from 'expo-status-bar'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
|
||||
SplashScreen.preventAutoHideAsync()
|
||||
|
||||
|
@ -69,26 +72,29 @@ function InnerApp() {
|
|||
|
||||
return (
|
||||
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
||||
{isAndroid && <StatusBar />}
|
||||
<Alf theme={theme}>
|
||||
<Splash isReady={!isInitialLoad}>
|
||||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
<StatsigProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</RootSiblingParent>
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
</Splash>
|
||||
</Alf>
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
import {Provider as UnreadNotifsProvider} from 'state/queries/notifications/unread'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {Provider as PortalProvider} from '#/components/Portal'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
import {useIntentHandler} from 'lib/hooks/useIntentHandler'
|
||||
|
||||
function InnerApp() {
|
||||
|
@ -54,21 +55,23 @@ function InnerApp() {
|
|||
<React.Fragment
|
||||
// Resets the entire tree below when it changes:
|
||||
key={currentAccount?.did}>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
<StatsigProvider>
|
||||
<LoggedOutViewProvider>
|
||||
<SelectedFeedProvider>
|
||||
<UnreadNotifsProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
{/* All components should be within this provider */}
|
||||
<RootSiblingParent>
|
||||
<SafeAreaProvider>
|
||||
<Shell />
|
||||
</SafeAreaProvider>
|
||||
</RootSiblingParent>
|
||||
<ToastContainer />
|
||||
</ThemeProvider>
|
||||
</UnreadNotifsProvider>
|
||||
</SelectedFeedProvider>
|
||||
</LoggedOutViewProvider>
|
||||
</StatsigProvider>
|
||||
</React.Fragment>
|
||||
</Alf>
|
||||
)
|
||||
|
|
|
@ -78,6 +78,7 @@ import {createNativeStackNavigatorWithAuth} from './view/shell/createNativeStack
|
|||
import {msg} from '@lingui/macro'
|
||||
import {i18n, MessageDescriptor} from '@lingui/core'
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {logEvent} from './lib/statsig/statsig'
|
||||
|
||||
const navigationRef = createNavigationContainerRef<AllNavigatorParams>()
|
||||
|
||||
|
@ -649,11 +650,14 @@ function logModuleInitTime() {
|
|||
return
|
||||
}
|
||||
didInit = true
|
||||
|
||||
const initMs = Math.round(
|
||||
// @ts-ignore Emitted by Metro in the bundle prelude
|
||||
performance.now() - global.__BUNDLE_START_TIME__,
|
||||
)
|
||||
console.log(`Time to first paint: ${initMs} ms`)
|
||||
logEvent('init', initMs)
|
||||
|
||||
if (__DEV__) {
|
||||
// This log is noisy, so keep false committed
|
||||
const shouldLog = false
|
||||
|
|
|
@ -27,7 +27,7 @@ export type ButtonColor =
|
|||
| 'gradient_sunset'
|
||||
| 'gradient_nordic'
|
||||
| 'gradient_bonfire'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'large'
|
||||
export type ButtonSize = 'tiny' | 'small' | 'medium' | 'large'
|
||||
export type ButtonShape = 'round' | 'square' | 'default'
|
||||
export type VariantProps = {
|
||||
/**
|
||||
|
@ -274,6 +274,8 @@ export function Button({
|
|||
if (shape === 'default') {
|
||||
if (size === 'large') {
|
||||
baseStyles.push({paddingVertical: 15}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'medium') {
|
||||
baseStyles.push({paddingVertical: 12}, a.px_2xl, a.rounded_sm, a.gap_md)
|
||||
} else if (size === 'small') {
|
||||
baseStyles.push({paddingVertical: 9}, a.px_lg, a.rounded_sm, a.gap_sm)
|
||||
} else if (size === 'tiny') {
|
||||
|
|
|
@ -31,14 +31,17 @@ export function useDialogControl(): DialogOuterProps['control'] {
|
|||
}
|
||||
}, [id, activeDialogs])
|
||||
|
||||
return {
|
||||
id,
|
||||
ref: control,
|
||||
open: () => {
|
||||
control.current.open()
|
||||
},
|
||||
close: cb => {
|
||||
control.current.close(cb)
|
||||
},
|
||||
}
|
||||
return React.useMemo<DialogOuterProps['control']>(
|
||||
() => ({
|
||||
id,
|
||||
ref: control,
|
||||
open: () => {
|
||||
control.current.open()
|
||||
},
|
||||
close: cb => {
|
||||
control.current.close(cb)
|
||||
},
|
||||
}),
|
||||
[id, control],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import {useTheme, atoms as a, flatten} from '#/alf'
|
|||
import {Portal} from '#/components/Portal'
|
||||
import {createInput} from '#/components/forms/TextField'
|
||||
import {logger} from '#/logger'
|
||||
import {useDialogStateContext} from '#/state/dialogs'
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
|
||||
import {
|
||||
DialogOuterProps,
|
||||
|
@ -82,7 +82,7 @@ export function Outer({
|
|||
const hasSnapPoints = !!sheetOptions.snapPoints
|
||||
const insets = useSafeAreaInsets()
|
||||
const closeCallback = React.useRef<() => void>()
|
||||
const {openDialogs} = useDialogStateContext()
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
|
||||
/*
|
||||
* 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']>(
|
||||
({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"
|
||||
setOpenIndex(index || 0)
|
||||
},
|
||||
[setOpenIndex, openDialogs, control.id],
|
||||
[setOpenIndex, setDialogIsOpen, control.id],
|
||||
)
|
||||
|
||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
||||
|
@ -119,65 +119,66 @@ export function Outer({
|
|||
[open, close],
|
||||
)
|
||||
|
||||
const onChange = React.useCallback(
|
||||
(index: number) => {
|
||||
if (index === -1) {
|
||||
Keyboard.dismiss()
|
||||
try {
|
||||
closeCallback.current?.()
|
||||
} catch (e: any) {
|
||||
logger.error(`Dialog closeCallback failed`, {
|
||||
message: e.message,
|
||||
})
|
||||
} finally {
|
||||
closeCallback.current = undefined
|
||||
}
|
||||
|
||||
openDialogs.current.delete(control.id)
|
||||
onClose?.()
|
||||
setOpenIndex(-1)
|
||||
}
|
||||
},
|
||||
[onClose, setOpenIndex, openDialogs, control.id],
|
||||
)
|
||||
const onCloseInner = React.useCallback(() => {
|
||||
Keyboard.dismiss()
|
||||
try {
|
||||
closeCallback.current?.()
|
||||
} catch (e: any) {
|
||||
logger.error(`Dialog closeCallback failed`, {
|
||||
message: e.message,
|
||||
})
|
||||
} finally {
|
||||
closeCallback.current = undefined
|
||||
}
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
setOpenIndex(-1)
|
||||
}, [control.id, onClose, setDialogIsOpen])
|
||||
|
||||
const context = React.useMemo(() => ({close}), [close])
|
||||
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onChange={onChange}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}>
|
||||
<BottomSheet
|
||||
enableDynamicSizing={!hasSnapPoints}
|
||||
enablePanDownToClose
|
||||
keyboardBehavior="interactive"
|
||||
android_keyboardInputMode="adjustResize"
|
||||
keyboardBlurBehavior="restore"
|
||||
topInset={insets.top}
|
||||
{...sheetOptions}
|
||||
snapPoints={sheetOptions.snapPoints || ['100%']}
|
||||
ref={sheet}
|
||||
index={openIndex}
|
||||
backgroundStyle={{backgroundColor: 'transparent'}}
|
||||
backdropComponent={Backdrop}
|
||||
handleIndicatorStyle={{backgroundColor: t.palette.primary_500}}
|
||||
handleStyle={{display: 'none'}}
|
||||
onClose={onCloseInner}>
|
||||
<Context.Provider value={context}>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.inset_0,
|
||||
t.atoms.bg,
|
||||
{
|
||||
borderTopLeftRadius: 40,
|
||||
borderTopRightRadius: 40,
|
||||
height: Dimensions.get('window').height * 2,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{children}
|
||||
</Context.Provider>
|
||||
</BottomSheet>
|
||||
</View>
|
||||
</Portal>
|
||||
)
|
||||
)
|
||||
|
|
|
@ -12,7 +12,7 @@ import {DialogOuterProps, DialogInnerProps} from '#/components/Dialog/types'
|
|||
import {Context} from '#/components/Dialog/context'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
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 * from '#/components/Dialog/types'
|
||||
|
@ -30,21 +30,21 @@ export function Outer({
|
|||
const {gtMobile} = useBreakpoints()
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const [isVisible, setIsVisible] = React.useState(true)
|
||||
const {openDialogs} = useDialogStateContext()
|
||||
const {setDialogIsOpen} = useDialogStateControlContext()
|
||||
|
||||
const open = React.useCallback(() => {
|
||||
setIsOpen(true)
|
||||
openDialogs.current.add(control.id)
|
||||
}, [setIsOpen, openDialogs, control.id])
|
||||
setDialogIsOpen(control.id, true)
|
||||
}, [setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
const close = React.useCallback(async () => {
|
||||
setIsVisible(false)
|
||||
await new Promise(resolve => setTimeout(resolve, 150))
|
||||
setIsOpen(false)
|
||||
setIsVisible(true)
|
||||
openDialogs.current.delete(control.id)
|
||||
setDialogIsOpen(control.id, false)
|
||||
onClose?.()
|
||||
}, [onClose, setIsOpen, openDialogs, control.id])
|
||||
}, [onClose, setIsOpen, setDialogIsOpen, control.id])
|
||||
|
||||
useImperativeHandle(
|
||||
control.ref,
|
||||
|
|
|
@ -22,6 +22,7 @@ export type DialogControlRefProps = {
|
|||
export type DialogControlProps = DialogControlRefProps & {
|
||||
id: string
|
||||
ref: React.RefObject<DialogControlRefProps>
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
||||
export type DialogContextProps = {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {View} from 'react-native'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Trans} from '@lingui/macro'
|
||||
import {cleanError} from 'lib/strings/errors'
|
||||
|
@ -143,7 +144,7 @@ export function ListMaybePlaceholder({
|
|||
}) {
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
const t = useTheme()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {gtMobile, gtTablet} = useBreakpoints()
|
||||
|
||||
const canGoBack = navigation.canGoBack()
|
||||
const onGoBack = React.useCallback(() => {
|
||||
|
@ -165,14 +166,16 @@ export function ListMaybePlaceholder({
|
|||
if (!isEmpty) return null
|
||||
|
||||
return (
|
||||
<View
|
||||
<CenteredView
|
||||
style={[
|
||||
a.flex_1,
|
||||
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,
|
||||
{paddingTop: 175, paddingBottom: 110},
|
||||
]}>
|
||||
]}
|
||||
sideBorders={gtMobile}
|
||||
topBorder={!gtTablet}>
|
||||
{isLoading ? (
|
||||
<View style={[a.w_full, a.align_center, {top: 100}]}>
|
||||
<Loader size="xl" />
|
||||
|
@ -241,6 +244,6 @@ export function ListMaybePlaceholder({
|
|||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
])}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -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 & {}>
|
|
@ -3,7 +3,7 @@ import {View, PressableProps} from 'react-native'
|
|||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {useTheme, atoms as a} from '#/alf'
|
||||
import {useTheme, atoms as a, useBreakpoints} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {Button} from '#/components/Button'
|
||||
|
||||
|
@ -25,6 +25,7 @@ export function Outer({
|
|||
}: React.PropsWithChildren<{
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const titleId = React.useId()
|
||||
const descriptionId = React.useId()
|
||||
|
||||
|
@ -38,12 +39,12 @@ export function Outer({
|
|||
<Context.Provider value={context}>
|
||||
<Dialog.Handle />
|
||||
|
||||
<Dialog.Inner
|
||||
<Dialog.ScrollableInner
|
||||
accessibilityLabelledBy={titleId}
|
||||
accessibilityDescribedBy={descriptionId}
|
||||
style={[{width: 'auto', maxWidth: 400}]}>
|
||||
style={[gtMobile ? {width: 'auto', maxWidth: 400} : a.w_full]}>
|
||||
{children}
|
||||
</Dialog.Inner>
|
||||
</Dialog.ScrollableInner>
|
||||
</Context.Provider>
|
||||
</Dialog.Outer>
|
||||
)
|
||||
|
@ -71,8 +72,16 @@ export function Description({children}: React.PropsWithChildren<{}>) {
|
|||
}
|
||||
|
||||
export function Actions({children}: React.PropsWithChildren<{}>) {
|
||||
const {gtMobile} = useBreakpoints()
|
||||
|
||||
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}
|
||||
</View>
|
||||
)
|
||||
|
@ -82,12 +91,13 @@ export function Cancel({
|
|||
children,
|
||||
}: React.PropsWithChildren<{onPress?: PressableProps['onPress']}>) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
return (
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
size="small"
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
label={_(msg`Cancel`)}
|
||||
onPress={() => close()}>
|
||||
{children}
|
||||
|
@ -100,6 +110,7 @@ export function Action({
|
|||
onPress,
|
||||
}: React.PropsWithChildren<{onPress?: () => void}>) {
|
||||
const {_} = useLingui()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {close} = Dialog.useDialogContext()
|
||||
const handleOnPress = React.useCallback(() => {
|
||||
close()
|
||||
|
@ -109,7 +120,7 @@ export function Action({
|
|||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="small"
|
||||
size={gtMobile ? 'small' : 'medium'}
|
||||
label={_(msg`Confirm`)}
|
||||
onPress={handleOnPress}>
|
||||
{children}
|
||||
|
|
|
@ -98,7 +98,7 @@ export function TagMenu({
|
|||
|
||||
control.close(() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -153,7 +153,7 @@ export function TagMenu({
|
|||
|
||||
control.close(() => {
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
tag: encodeURIComponent(tag),
|
||||
author: authorHandle,
|
||||
})
|
||||
})
|
||||
|
|
|
@ -66,7 +66,7 @@ export function TagMenu({
|
|||
label: _(msg`See ${truncatedTag} posts`),
|
||||
onPress() {
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
tag: encodeURIComponent(tag),
|
||||
})
|
||||
},
|
||||
testID: 'tagMenuSearch',
|
||||
|
@ -83,7 +83,7 @@ export function TagMenu({
|
|||
label: _(msg`See ${truncatedTag} posts by user`),
|
||||
onPress() {
|
||||
navigation.push('Hashtag', {
|
||||
tag: tag.replaceAll('#', '%23'),
|
||||
tag: encodeURIComponent(tag),
|
||||
author: authorHandle,
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import React from 'react'
|
||||
import {View, Pressable} from 'react-native'
|
||||
import DateTimePicker, {
|
||||
BaseProps as DateTimePickerProps,
|
||||
} from '@react-native-community/datetimepicker'
|
||||
|
||||
import {useTheme, atoms} from '#/alf'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
@ -15,6 +12,8 @@ import {
|
|||
localizeDate,
|
||||
toSimpleDateString,
|
||||
} from '#/components/forms/DateField/utils'
|
||||
import DatePicker from 'react-native-date-picker'
|
||||
import {isAndroid} from 'platform/detection'
|
||||
|
||||
export * as utils from '#/components/forms/DateField/utils'
|
||||
export const Label = TextField.Label
|
||||
|
@ -38,20 +37,20 @@ export function DateField({
|
|||
const {chromeFocus, chromeError, chromeErrorHover} =
|
||||
TextField.useSharedInputStyles()
|
||||
|
||||
const onChangeInternal = React.useCallback<
|
||||
Required<DateTimePickerProps>['onChange']
|
||||
>(
|
||||
(_event, date) => {
|
||||
const onChangeInternal = React.useCallback(
|
||||
(date: Date) => {
|
||||
setOpen(false)
|
||||
|
||||
if (date) {
|
||||
const formatted = toSimpleDateString(date)
|
||||
onChangeDate(formatted)
|
||||
}
|
||||
const formatted = toSimpleDateString(date)
|
||||
onChangeDate(formatted)
|
||||
},
|
||||
[onChangeDate, setOpen],
|
||||
)
|
||||
|
||||
const onCancel = React.useCallback(() => {
|
||||
setOpen(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View style={[atoms.relative, atoms.w_full]}>
|
||||
<Pressable
|
||||
|
@ -89,18 +88,18 @@ export function DateField({
|
|||
</Pressable>
|
||||
|
||||
{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}
|
||||
accessibilityLabel={label}
|
||||
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>
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from '@react-native-community/datetimepicker'
|
||||
|
||||
import {useTheme, atoms} from '#/alf'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import {toSimpleDateString} from '#/components/forms/DateField/utils'
|
||||
import {DateFieldProps} from '#/components/forms/DateField/types'
|
||||
import DatePicker from 'react-native-date-picker'
|
||||
|
||||
export * as utils from '#/components/forms/DateField/utils'
|
||||
export const Label = TextField.Label
|
||||
|
@ -28,7 +26,7 @@ export function DateField({
|
|||
const t = useTheme()
|
||||
|
||||
const onChangeInternal = React.useCallback(
|
||||
(event: DateTimePickerEvent, date: Date | undefined) => {
|
||||
(date: Date | undefined) => {
|
||||
if (date) {
|
||||
const formatted = toSimpleDateString(date)
|
||||
onChangeDate(formatted)
|
||||
|
@ -39,17 +37,15 @@ export function DateField({
|
|||
|
||||
return (
|
||||
<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}
|
||||
accessibilityLabel={label}
|
||||
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>
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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',
|
||||
})
|
|
@ -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])
|
||||
}
|
|
@ -2,9 +2,15 @@ import {RouteParams, Route} from './types'
|
|||
|
||||
export class Router {
|
||||
routes: [string, Route][] = []
|
||||
constructor(description: Record<string, string>) {
|
||||
constructor(description: Record<string, string | string[]>) {
|
||||
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)])
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -3,6 +3,8 @@ import {BSKY_SERVICE} from 'lib/constants'
|
|||
import TLDs from 'tlds'
|
||||
import psl from 'psl'
|
||||
|
||||
export const BSKY_APP_HOST = 'https://bsky.app'
|
||||
|
||||
export function isValidDomain(str: string): boolean {
|
||||
return !!TLDs.find(tld => {
|
||||
let i = str.lastIndexOf(tld)
|
||||
|
@ -67,8 +69,21 @@ export function isBskyAppUrl(url: string): boolean {
|
|||
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 {
|
||||
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 {
|
||||
|
@ -148,6 +163,11 @@ export function feedUriToHref(url: string): string {
|
|||
export function linkRequiresWarning(uri: string, label: string) {
|
||||
const labelDomain = labelToDomain(label)
|
||||
|
||||
// If the uri started with a / we know it is internal.
|
||||
if (isRelativeUrl(uri)) {
|
||||
return false
|
||||
}
|
||||
|
||||
let urip
|
||||
try {
|
||||
urip = new URL(uri)
|
||||
|
@ -156,9 +176,12 @@ export function linkRequiresWarning(uri: string, label: string) {
|
|||
}
|
||||
|
||||
const host = urip.hostname.toLowerCase()
|
||||
|
||||
// Hosts that end with bsky.app or bsky.social should be trusted by default.
|
||||
if (host.endsWith('bsky.app') || host.endsWith('bsky.social')) {
|
||||
if (
|
||||
host.endsWith('bsky.app') ||
|
||||
host.endsWith('bsky.social') ||
|
||||
host.endsWith('blueskyweb.xyz')
|
||||
) {
|
||||
// if this is a link to internal content,
|
||||
// warn if it represents itself as a URL to another app
|
||||
return !!labelDomain && labelDomain !== host && isPossiblyAUrl(labelDomain)
|
||||
|
@ -214,3 +237,8 @@ export function splitApexDomain(hostname: string): [string, string] {
|
|||
hostnamep.domain,
|
||||
]
|
||||
}
|
||||
|
||||
export function createBskyAppAbsoluteUrl(path: string): string {
|
||||
const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '')
|
||||
return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}`
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
// @ts-ignore Web-only. On RN, this is set by Metro.
|
||||
window.__BUNDLE_START_TIME__ = performance.now()
|
|
@ -12,7 +12,7 @@ export const router = new Router({
|
|||
ModerationModlists: '/moderation/modlists',
|
||||
ModerationMutedAccounts: '/moderation/muted-accounts',
|
||||
ModerationBlockedAccounts: '/moderation/blocked-accounts',
|
||||
Profile: '/profile/:name',
|
||||
Profile: ['/profile/:name', '/profile/:name/rss'],
|
||||
ProfileFollowers: '/profile/:name/followers',
|
||||
ProfileFollows: '/profile/:name/follows',
|
||||
ProfileList: '/profile/:name/lists/:rkey',
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import React from 'react'
|
||||
import {ListRenderItemInfo, Pressable} from 'react-native'
|
||||
import {atoms as a, useBreakpoints} from '#/alf'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
import {useSetMinimalShellMode} from 'state/shell'
|
||||
import {ViewHeader} from 'view/com/util/ViewHeader'
|
||||
|
@ -19,11 +18,11 @@ import {List} from 'view/com/util/List'
|
|||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {sanitizeHandle} from 'lib/strings/handles'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {isNative} from 'platform/detection'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
|
||||
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
|
||||
return <Post post={item} />
|
||||
|
@ -38,12 +37,12 @@ export default function HashtagScreen({
|
|||
}: NativeStackScreenProps<CommonNavigatorParams, 'Hashtag'>) {
|
||||
const {tag, author} = route.params
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {_} = useLingui()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const [isPTR, setIsPTR] = React.useState(false)
|
||||
|
||||
const fullTag = React.useMemo(() => {
|
||||
return `#${tag.replaceAll('%23', '#')}`
|
||||
return `#${decodeURIComponent(tag)}`
|
||||
}, [tag])
|
||||
|
||||
const queryParam = React.useMemo(() => {
|
||||
|
@ -84,7 +83,7 @@ export default function HashtagScreen({
|
|||
|
||||
const onShare = React.useCallback(() => {
|
||||
const url = new URL('https://bsky.app')
|
||||
url.pathname = `/hashtag/${tag}`
|
||||
url.pathname = `/hashtag/${decodeURIComponent(tag)}`
|
||||
if (author) {
|
||||
url.searchParams.set('author', author)
|
||||
}
|
||||
|
@ -103,7 +102,7 @@ export default function HashtagScreen({
|
|||
}, [isFetching, hasNextPage, error, fetchNextPage])
|
||||
|
||||
return (
|
||||
<CenteredView style={a.flex_1} sideBorders={gtMobile}>
|
||||
<>
|
||||
<ViewHeader
|
||||
title={headerTitle}
|
||||
subtitle={author ? _(msg`From @${sanitizedAuthor}`) : undefined}
|
||||
|
@ -157,8 +156,10 @@ export default function HashtagScreen({
|
|||
onRetry={fetchNextPage}
|
||||
/>
|
||||
}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
/>
|
||||
)}
|
||||
</CenteredView>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react'
|
||||
import {SharedValue, useSharedValue} from 'react-native-reanimated'
|
||||
import {DialogControlRefProps} from '#/components/Dialog'
|
||||
import {Provider as GlobalDialogsProvider} from '#/components/dialogs/Context'
|
||||
|
||||
const DialogContext = React.createContext<{
|
||||
interface IDialogContext {
|
||||
/**
|
||||
* The currently active `useDialogControl` hooks.
|
||||
*/
|
||||
|
@ -14,19 +15,24 @@ const DialogContext = React.createContext<{
|
|||
* `useId`.
|
||||
*/
|
||||
openDialogs: React.MutableRefObject<Set<string>>
|
||||
}>({
|
||||
activeDialogs: {
|
||||
current: new Map(),
|
||||
},
|
||||
openDialogs: {
|
||||
current: new Set(),
|
||||
},
|
||||
})
|
||||
/**
|
||||
* The counterpart to `accessibilityViewIsModal` for Android. This property
|
||||
* applies to the parent of all non-modal views, and prevents TalkBack from
|
||||
* navigating within content beneath an open dialog.
|
||||
*
|
||||
* @see https://reactnative.dev/docs/accessibility#importantforaccessibility-android
|
||||
*/
|
||||
importantForAccessibility: SharedValue<'auto' | 'no-hide-descendants'>
|
||||
}
|
||||
|
||||
const DialogContext = React.createContext<IDialogContext>({} as IDialogContext)
|
||||
|
||||
const DialogControlContext = React.createContext<{
|
||||
closeAllDialogs(): boolean
|
||||
setDialogIsOpen(id: string, isOpen: boolean): void
|
||||
}>({
|
||||
closeAllDialogs: () => false,
|
||||
setDialogIsOpen: () => {},
|
||||
})
|
||||
|
||||
export function useDialogStateContext() {
|
||||
|
@ -42,14 +48,46 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
|
|||
Map<string, React.MutableRefObject<DialogControlRefProps>>
|
||||
>(new Map())
|
||||
const openDialogs = React.useRef<Set<string>>(new Set())
|
||||
const importantForAccessibility = useSharedValue<
|
||||
'auto' | 'no-hide-descendants'
|
||||
>('auto')
|
||||
|
||||
const closeAllDialogs = React.useCallback(() => {
|
||||
activeDialogs.current.forEach(dialog => dialog.current.close())
|
||||
return openDialogs.current.size > 0
|
||||
}, [])
|
||||
|
||||
const context = React.useMemo(() => ({activeDialogs, openDialogs}), [])
|
||||
const controls = React.useMemo(() => ({closeAllDialogs}), [closeAllDialogs])
|
||||
const setDialogIsOpen = React.useCallback(
|
||||
(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 (
|
||||
<DialogContext.Provider value={context}>
|
||||
|
|
|
@ -5,6 +5,11 @@ import * as WebBrowser from 'expo-web-browser'
|
|||
import {isNative} from '#/platform/detection'
|
||||
import {useModalControls} from '../modals'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {
|
||||
isBskyRSSUrl,
|
||||
isRelativeUrl,
|
||||
createBskyAppAbsoluteUrl,
|
||||
} from 'lib/strings/url-helpers'
|
||||
|
||||
type StateContext = persisted.Schema['useInAppBrowser']
|
||||
type SetContext = (v: persisted.Schema['useInAppBrowser']) => void
|
||||
|
@ -57,6 +62,10 @@ export function useOpenLink() {
|
|||
|
||||
const openLink = React.useCallback(
|
||||
(url: string, override?: boolean) => {
|
||||
if (isBskyRSSUrl(url) && isRelativeUrl(url)) {
|
||||
url = createBskyAppAbsoluteUrl(url)
|
||||
}
|
||||
|
||||
if (isNative && !url.startsWith('mailto:')) {
|
||||
if (override === undefined && enabled === undefined) {
|
||||
openModal({
|
||||
|
|
|
@ -7,7 +7,7 @@ import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/
|
|||
export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
|
||||
{
|
||||
hideReplies: false,
|
||||
hideRepliesByUnfollowed: false,
|
||||
hideRepliesByUnfollowed: true,
|
||||
hideRepliesByLikeCount: 0,
|
||||
hideReposts: false,
|
||||
hideQuotePosts: false,
|
||||
|
|
|
@ -169,7 +169,7 @@ export function usePreferencesSetBirthDateMutation() {
|
|||
|
||||
return useMutation<void, unknown, {birthDate: Date}>({
|
||||
mutationFn: async ({birthDate}: {birthDate: Date}) => {
|
||||
await getAgent().setPersonalDetails({birthDate})
|
||||
await getAgent().setPersonalDetails({birthDate: birthDate.toISOString()})
|
||||
// triggers a refetch
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: preferencesQueryKey,
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
Keyboard,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import {CreateAccountState, CreateAccountDispatch, is18} from './state'
|
||||
|
@ -19,7 +18,6 @@ import {ErrorMessage} from 'view/com/util/error/ErrorMessage'
|
|||
import {isWeb} from 'platform/detection'
|
||||
import {Trans, msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {logger} from '#/logger'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
|
@ -49,7 +47,6 @@ export function Step1({
|
|||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {_} = useLingui()
|
||||
const {openModal} = useModalControls()
|
||||
const serverInputControl = useDialogControl()
|
||||
|
||||
const onPressSelectService = React.useCallback(() => {
|
||||
|
@ -57,10 +54,6 @@ export function Step1({
|
|||
Keyboard.dismiss()
|
||||
}, [serverInputControl])
|
||||
|
||||
const onPressWaitlist = React.useCallback(() => {
|
||||
openModal({name: 'waitlist'})
|
||||
}, [openModal])
|
||||
|
||||
const birthDate = React.useMemo(() => {
|
||||
return sanitizeDate(uiState.birthDate)
|
||||
}, [uiState.birthDate])
|
||||
|
@ -164,23 +157,7 @@ export function Step1({
|
|||
</View>
|
||||
)}
|
||||
|
||||
{!uiState.inviteCode && uiState.isInviteCodeRequired ? (
|
||||
<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>
|
||||
) : (
|
||||
{!uiState.isInviteCodeRequired || uiState.inviteCode ? (
|
||||
<>
|
||||
<View style={s.pb20}>
|
||||
<Text
|
||||
|
@ -260,7 +237,7 @@ export function Step1({
|
|||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
) : undefined}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react'
|
||||
import {StyleSheet, View} from 'react-native'
|
||||
import Animated from 'react-native-reanimated'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
||||
import {HomeHeaderLayoutMobile} from './HomeHeaderLayoutMobile'
|
||||
|
@ -12,6 +13,8 @@ import {
|
|||
import {useLingui} from '@lingui/react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {CogIcon} from '#/lib/icons'
|
||||
import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
|
||||
export function HomeHeaderLayout(props: {
|
||||
children: React.ReactNode
|
||||
|
@ -33,6 +36,8 @@ function HomeHeaderLayoutDesktopAndTablet({
|
|||
tabBarAnchor: JSX.Element | null | undefined
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const {headerMinimalShellTransform} = useMinimalShellMode()
|
||||
const {headerHeight} = useShellLayout()
|
||||
const {_} = useLingui()
|
||||
|
||||
return (
|
||||
|
@ -60,9 +65,19 @@ function HomeHeaderLayoutDesktopAndTablet({
|
|||
</Link>
|
||||
</View>
|
||||
{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}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,6 @@ import * as ReportModal from './report/Modal'
|
|||
import * as AppealLabelModal from './AppealLabel'
|
||||
import * as DeleteAccountModal from './DeleteAccount'
|
||||
import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as WaitlistModal from './Waitlist'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as AddAppPassword from './AddAppPasswords'
|
||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
|
@ -109,9 +108,6 @@ export function ModalsContainer() {
|
|||
} else if (activeModal?.name === 'change-handle') {
|
||||
snapPoints = ChangeHandleModal.snapPoints
|
||||
element = <ChangeHandleModal.Component {...activeModal} />
|
||||
} else if (activeModal?.name === 'waitlist') {
|
||||
snapPoints = WaitlistModal.snapPoints
|
||||
element = <WaitlistModal.Component />
|
||||
} else if (activeModal?.name === 'invite-codes') {
|
||||
snapPoints = InviteCodesModal.snapPoints
|
||||
element = <InviteCodesModal.Component />
|
||||
|
|
|
@ -22,7 +22,6 @@ import * as CropImageModal from './crop-image/CropImage.web'
|
|||
import * as AltTextImageModal from './AltImage'
|
||||
import * as EditImageModal from './EditImage'
|
||||
import * as ChangeHandleModal from './ChangeHandle'
|
||||
import * as WaitlistModal from './Waitlist'
|
||||
import * as InviteCodesModal from './InviteCodes'
|
||||
import * as AddAppPassword from './AddAppPasswords'
|
||||
import * as ContentFilteringSettingsModal from './ContentFilteringSettings'
|
||||
|
@ -105,8 +104,6 @@ function Modal({modal}: {modal: ModalIface}) {
|
|||
element = <ThreadgateModal.Component {...modal} />
|
||||
} else if (modal.name === 'change-handle') {
|
||||
element = <ChangeHandleModal.Component {...modal} />
|
||||
} else if (modal.name === 'waitlist') {
|
||||
element = <WaitlistModal.Component />
|
||||
} else if (modal.name === 'invite-codes') {
|
||||
element = <InviteCodesModal.Component />
|
||||
} else if (modal.name === 'add-app-password') {
|
||||
|
|
|
@ -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'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,
|
||||
},
|
||||
})
|
|
@ -228,6 +228,7 @@ let FeedItem = ({
|
|||
text={sanitizeDisplayName(
|
||||
authors[0].displayName || authors[0].handle,
|
||||
)}
|
||||
disableMismatchWarning
|
||||
/>
|
||||
{authors.length > 1 ? (
|
||||
<>
|
||||
|
|
|
@ -32,6 +32,7 @@ import {msg} from '@lingui/macro'
|
|||
import {useLingui} from '@lingui/react'
|
||||
import {DiscoverFallbackHeader} from './DiscoverFallbackHeader'
|
||||
import {FALLBACK_MARKER_POST} from '#/lib/api/feed/home'
|
||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||
|
||||
const LOADING_ITEM = {_reactKey: '__loading__'}
|
||||
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
|
||||
|
@ -84,6 +85,7 @@ let Feed = ({
|
|||
const {_} = useLingui()
|
||||
const queryClient = useQueryClient()
|
||||
const {currentAccount} = useSession()
|
||||
const initialNumToRender = useInitialNumToRender()
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const checkForNewRef = React.useRef<(() => void) | null>(null)
|
||||
const lastFetchRef = React.useRef<number>(Date.now())
|
||||
|
@ -327,6 +329,8 @@ let Feed = ({
|
|||
desktopFixedHeight={
|
||||
desktopFixedHeightOffset ? desktopFixedHeightOffset : true
|
||||
}
|
||||
initialNumToRender={initialNumToRender}
|
||||
windowSize={11}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
|
|
|
@ -8,7 +8,14 @@ import {View, ViewStyle} from 'react-native'
|
|||
export function EventStopper({
|
||||
children,
|
||||
style,
|
||||
}: React.PropsWithChildren<{style?: ViewStyle | ViewStyle[]}>) {
|
||||
onKeyDown = true,
|
||||
}: React.PropsWithChildren<{
|
||||
style?: ViewStyle | ViewStyle[]
|
||||
/**
|
||||
* Default `true`. Set to `false` to allow onKeyDown to propagate
|
||||
*/
|
||||
onKeyDown?: boolean
|
||||
}>) {
|
||||
const stop = (e: any) => {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
@ -18,7 +25,7 @@ export function EventStopper({
|
|||
onTouchEnd={stop}
|
||||
// @ts-ignore web only -prf
|
||||
onClick={stop}
|
||||
onKeyDown={stop}
|
||||
onKeyDown={onKeyDown ? stop : undefined}
|
||||
style={style}>
|
||||
{children}
|
||||
</View>
|
||||
|
|
|
@ -5,4 +5,6 @@ export function CenteredView({
|
|||
style,
|
||||
sideBorders,
|
||||
...props
|
||||
}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>)
|
||||
}: React.PropsWithChildren<
|
||||
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
|
||||
>)
|
||||
|
|
|
@ -32,8 +32,11 @@ interface AddedProps {
|
|||
export function CenteredView({
|
||||
style,
|
||||
sideBorders,
|
||||
topBorder,
|
||||
...props
|
||||
}: React.PropsWithChildren<ViewProps & {sideBorders?: boolean}>) {
|
||||
}: React.PropsWithChildren<
|
||||
ViewProps & {sideBorders?: boolean; topBorder?: boolean}
|
||||
>) {
|
||||
const pal = usePalette('default')
|
||||
const {isMobile} = useWebMediaQueries()
|
||||
if (!isMobile) {
|
||||
|
@ -46,6 +49,12 @@ export function CenteredView({
|
|||
})
|
||||
style = addStyle(style, pal.border)
|
||||
}
|
||||
if (topBorder) {
|
||||
style = addStyle(style, {
|
||||
borderTopWidth: 1,
|
||||
})
|
||||
style = addStyle(style, pal.border)
|
||||
}
|
||||
return <View style={style} {...props} />
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import React, {useState, useCallback} from 'react'
|
||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
||||
import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from '@react-native-community/datetimepicker'
|
||||
import {
|
||||
FontAwesomeIcon,
|
||||
FontAwesomeIconStyle,
|
||||
|
@ -14,6 +11,7 @@ import {TypographyVariant} from 'lib/ThemeContext'
|
|||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {getLocales} from 'expo-localization'
|
||||
import DatePicker from 'react-native-date-picker'
|
||||
|
||||
const LOCALE = getLocales()[0]
|
||||
|
||||
|
@ -43,11 +41,9 @@ export function DateInput(props: Props) {
|
|||
}, [props.handleAsUTC])
|
||||
|
||||
const onChangeInternal = useCallback(
|
||||
(event: DateTimePickerEvent, date: Date | undefined) => {
|
||||
(date: Date) => {
|
||||
setShow(false)
|
||||
if (date) {
|
||||
props.onChange(date)
|
||||
}
|
||||
props.onChange(date)
|
||||
},
|
||||
[setShow, props],
|
||||
)
|
||||
|
@ -56,6 +52,10 @@ export function DateInput(props: Props) {
|
|||
setShow(true)
|
||||
}, [setShow])
|
||||
|
||||
const onCancel = useCallback(() => {
|
||||
setShow(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<View>
|
||||
{isAndroid && (
|
||||
|
@ -80,15 +80,17 @@ export function DateInput(props: Props) {
|
|||
</Button>
|
||||
)}
|
||||
{(isIOS || show) && (
|
||||
<DateTimePicker
|
||||
testID={props.testID ? `${props.testID}-datepicker` : undefined}
|
||||
<DatePicker
|
||||
timeZoneOffsetInMinutes={0}
|
||||
modal={isAndroid}
|
||||
open={isAndroid}
|
||||
theme={theme.colorScheme}
|
||||
date={props.value}
|
||||
onDateChange={onChangeInternal}
|
||||
onConfirm={onChangeInternal}
|
||||
onCancel={onCancel}
|
||||
mode="date"
|
||||
timeZoneName={props.handleAsUTC ? 'Etc/UTC' : undefined}
|
||||
display="spinner"
|
||||
// @ts-ignore applies in iOS only -prf
|
||||
themeVariant={theme.colorScheme}
|
||||
value={props.value}
|
||||
onChange={onChangeInternal}
|
||||
testID={props.testID ? `${props.testID}-datepicker` : undefined}
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityLabelledBy={props.accessibilityLabelledBy}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import React, {memo} from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {
|
||||
StyleProp,
|
||||
ViewStyle,
|
||||
Pressable,
|
||||
View,
|
||||
PressableProps,
|
||||
} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {useNavigation} from '@react-navigation/native'
|
||||
|
@ -12,10 +18,6 @@ import {
|
|||
import {toShareUrl} from 'lib/strings/url-helpers'
|
||||
import {useTheme} from 'lib/ThemeContext'
|
||||
import {shareUrl} from 'lib/sharing'
|
||||
import {
|
||||
NativeDropdown,
|
||||
DropdownItem as NativeDropdownItem,
|
||||
} from './NativeDropdown'
|
||||
import * as Toast from '../Toast'
|
||||
import {EventStopper} from '../EventStopper'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
|
@ -36,6 +38,19 @@ import {isWeb} from '#/platform/detection'
|
|||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||
import {useGlobalDialogsControlContext} from '#/components/dialogs/Context'
|
||||
|
||||
import {atoms as a, useTheme as useAlf, web} from '#/alf'
|
||||
import * as Menu from '#/components/Menu'
|
||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||
import {Filter_Stroke2_Corner0_Rounded as Filter} from '#/components/icons/Filter'
|
||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as Share} from '#/components/icons/ArrowOutOfBox'
|
||||
import {EyeSlash_Stroke2_Corner0_Rounded as EyeSlash} from '#/components/icons/EyeSlash'
|
||||
import {Mute_Stroke2_Corner0_Rounded as Mute} from '#/components/icons/Mute'
|
||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||
import {BubbleQuestion_Stroke2_Corner0_Rounded as Translate} from '#/components/icons/Bubble'
|
||||
import {Warning_Stroke2_Corner0_Rounded as Warning} from '#/components/icons/Warning'
|
||||
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||
|
||||
let PostDropdownBtn = ({
|
||||
testID,
|
||||
postAuthor,
|
||||
|
@ -45,6 +60,7 @@ let PostDropdownBtn = ({
|
|||
richText,
|
||||
style,
|
||||
showAppealLabelItem,
|
||||
hitSlop,
|
||||
}: {
|
||||
testID: string
|
||||
postAuthor: AppBskyActorDefs.ProfileViewBasic
|
||||
|
@ -54,9 +70,11 @@ let PostDropdownBtn = ({
|
|||
richText: RichTextAPI
|
||||
style?: StyleProp<ViewStyle>
|
||||
showAppealLabelItem?: boolean
|
||||
hitSlop?: PressableProps['hitSlop']
|
||||
}): React.ReactNode => {
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const theme = useTheme()
|
||||
const alf = useAlf()
|
||||
const {_} = useLingui()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
const {openModal} = useModalControls()
|
||||
|
@ -151,173 +169,189 @@ let PostDropdownBtn = ({
|
|||
hidePost({uri: postUri})
|
||||
}, [postUri, hidePost])
|
||||
|
||||
const dropdownItems: NativeDropdownItem[] = [
|
||||
{
|
||||
label: _(msg`Translate`),
|
||||
onPress() {
|
||||
onOpenTranslate()
|
||||
},
|
||||
testID: 'postDropdownTranslateBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'character.book.closed',
|
||||
},
|
||||
android: 'ic_menu_sort_alphabetically',
|
||||
web: 'language',
|
||||
},
|
||||
},
|
||||
{
|
||||
label: _(msg`Copy post text`),
|
||||
onPress() {
|
||||
onCopyPostText()
|
||||
},
|
||||
testID: 'postDropdownCopyTextBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'doc.on.doc',
|
||||
},
|
||||
android: 'ic_menu_edit',
|
||||
web: ['far', 'paste'],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: isWeb ? _(msg`Copy link to post`) : _(msg`Share`),
|
||||
onPress() {
|
||||
const url = toShareUrl(href)
|
||||
shareUrl(url)
|
||||
},
|
||||
testID: 'postDropdownShareBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'square.and.arrow.up',
|
||||
},
|
||||
android: 'ic_menu_share',
|
||||
web: 'share',
|
||||
},
|
||||
},
|
||||
hasSession && {
|
||||
label: 'separator',
|
||||
},
|
||||
hasSession && {
|
||||
label: isThreadMuted ? _(msg`Unmute thread`) : _(msg`Mute thread`),
|
||||
onPress() {
|
||||
onToggleThreadMute()
|
||||
},
|
||||
testID: 'postDropdownMuteThreadBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'comment-slash',
|
||||
},
|
||||
},
|
||||
hasSession && {
|
||||
label: _(msg`Mute words & tags`),
|
||||
onPress() {
|
||||
mutedWordsDialogControl.open()
|
||||
},
|
||||
testID: 'postDropdownMuteWordsBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'speaker.slash',
|
||||
},
|
||||
android: 'ic_lock_silent_mode',
|
||||
web: 'filter',
|
||||
},
|
||||
},
|
||||
hasSession &&
|
||||
!isAuthor &&
|
||||
!isPostHidden && {
|
||||
label: _(msg`Hide post`),
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Hide this post?`),
|
||||
message: _(msg`This will hide this post from your feeds.`),
|
||||
onPressConfirm: onHidePost,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownHideBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'eye.slash',
|
||||
},
|
||||
android: 'ic_menu_delete',
|
||||
web: ['far', 'eye-slash'],
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'separator',
|
||||
},
|
||||
!isAuthor &&
|
||||
hasSession && {
|
||||
label: _(msg`Report post`),
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'report',
|
||||
uri: postUri,
|
||||
cid: postCid,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownReportBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
isAuthor && {
|
||||
label: _(msg`Delete post`),
|
||||
onPress() {
|
||||
openModal({
|
||||
name: 'confirm',
|
||||
title: _(msg`Delete this post?`),
|
||||
message: _(msg`Are you sure? This cannot be undone.`),
|
||||
onPressConfirm: onDeletePost,
|
||||
})
|
||||
},
|
||||
testID: 'postDropdownDeleteBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'trash',
|
||||
},
|
||||
android: 'ic_menu_delete',
|
||||
web: ['far', 'trash-can'],
|
||||
},
|
||||
},
|
||||
showAppealLabelItem && {
|
||||
label: 'separator',
|
||||
},
|
||||
showAppealLabelItem && {
|
||||
label: _(msg`Appeal content warning`),
|
||||
onPress() {
|
||||
openModal({name: 'appeal-label', uri: postUri, cid: postCid})
|
||||
},
|
||||
testID: 'postDropdownAppealBtn',
|
||||
icon: {
|
||||
ios: {
|
||||
name: 'exclamationmark.triangle',
|
||||
},
|
||||
android: 'ic_menu_report_image',
|
||||
web: 'circle-exclamation',
|
||||
},
|
||||
},
|
||||
].filter(Boolean) as NativeDropdownItem[]
|
||||
|
||||
return (
|
||||
<EventStopper>
|
||||
<NativeDropdown
|
||||
testID={testID}
|
||||
items={dropdownItems}
|
||||
accessibilityLabel={_(msg`More post options`)}
|
||||
accessibilityHint="">
|
||||
<View style={style}>
|
||||
<FontAwesomeIcon icon="ellipsis" size={20} color={defaultCtrlColor} />
|
||||
</View>
|
||||
</NativeDropdown>
|
||||
<EventStopper onKeyDown={false}>
|
||||
<Menu.Root>
|
||||
<Menu.Trigger label={_(msg`Open post options menu`)}>
|
||||
{({props, state}) => {
|
||||
const styles = [
|
||||
style,
|
||||
a.rounded_full,
|
||||
(state.hovered || state.focused || state.pressed) && [
|
||||
web({outline: 0}),
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -57,6 +57,7 @@ const styles = StyleSheet.create({
|
|||
btn: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
borderWidth: 1,
|
||||
borderLeftWidth: 0,
|
||||
paddingHorizontal: 10,
|
||||
|
|
|
@ -212,9 +212,7 @@ let PostCtrls = ({
|
|||
style={[styles.btn]}
|
||||
onPress={onShare}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={`${
|
||||
post.viewer?.like ? _(msg`Unlike`) : _(msg`Like`)
|
||||
} (${post.likeCount} ${pluralize(post.likeCount || 0, 'like')})`}
|
||||
accessibilityLabel={`${_(msg`Share`)}`}
|
||||
accessibilityHint=""
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}>
|
||||
<ArrowOutOfBox style={[defaultCtrlColor, styles.mt1]} width={22} />
|
||||
|
@ -231,6 +229,7 @@ let PostCtrls = ({
|
|||
richText={richText}
|
||||
showAppealLabelItem={showAppealLabelItem}
|
||||
style={styles.btnPad}
|
||||
hitSlop={big ? HITSLOP_20 : HITSLOP_10}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -16,6 +16,7 @@ import {Dialogs} from './Dialogs'
|
|||
import {Breakpoints} from './Breakpoints'
|
||||
import {Shadows} from './Shadows'
|
||||
import {Icons} from './Icons'
|
||||
import {Menus} from './Menus'
|
||||
|
||||
export function Storybook() {
|
||||
const t = useTheme()
|
||||
|
@ -84,6 +85,7 @@ export function Storybook() {
|
|||
<Links />
|
||||
<Forms />
|
||||
<Dialogs />
|
||||
<Menus />
|
||||
<Breakpoints />
|
||||
</View>
|
||||
</CenteredView>
|
||||
|
|
|
@ -391,7 +391,7 @@ export function DesktopLeftNav() {
|
|||
<FontAwesomeIcon
|
||||
icon="hand"
|
||||
style={pal.text as FontAwesomeIconStyle}
|
||||
size={isDesktop ? 20 : 26}
|
||||
size={isDesktop ? 23 : 26}
|
||||
/>
|
||||
}
|
||||
label={_(msg`Moderation`)}
|
||||
|
|
|
@ -30,6 +30,8 @@ import {useCloseAnyActiveElement} from '#/state/util'
|
|||
import * as notifications from 'lib/notifications/notifications'
|
||||
import {Outlet as PortalOutlet} from '#/components/Portal'
|
||||
import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
|
||||
import {useDialogStateContext} from 'state/dialogs'
|
||||
import Animated from 'react-native-reanimated'
|
||||
|
||||
function ShellInner() {
|
||||
const isDrawerOpen = useIsDrawerOpen()
|
||||
|
@ -53,6 +55,7 @@ function ShellInner() {
|
|||
const canGoBack = useNavigationState(state => !isStateAtTabRoot(state))
|
||||
const {hasSession, currentAccount} = useSession()
|
||||
const closeAnyActiveElement = useCloseAnyActiveElement()
|
||||
const {importantForAccessibility} = useDialogStateContext()
|
||||
// start undefined
|
||||
const currentAccountDid = React.useRef<string | undefined>(undefined)
|
||||
|
||||
|
@ -80,7 +83,9 @@ function ShellInner() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<View style={containerPadding}>
|
||||
<Animated.View
|
||||
style={containerPadding}
|
||||
importantForAccessibility={importantForAccessibility}>
|
||||
<ErrorBoundary>
|
||||
<Drawer
|
||||
renderDrawerContent={renderDrawerContent}
|
||||
|
@ -92,7 +97,7 @@ function ShellInner() {
|
|||
<TabsNavigator />
|
||||
</Drawer>
|
||||
</ErrorBoundary>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<Composer winHeight={winDim.height} />
|
||||
<ModalsContainer />
|
||||
<MutedWordsDialog />
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
height: calc(100% + env(safe-area-inset-top));
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
html, body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
/* Buttons and inputs have a font set by UA, so we'll have to reset that */
|
||||
button, input, textarea {
|
||||
|
@ -217,6 +220,7 @@
|
|||
}
|
||||
|
||||
/* NativeDropdown component */
|
||||
.radix-dropdown-item:focus,
|
||||
.nativeDropdown-item:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
275
yarn.lock
275
yarn.lock
|
@ -34,10 +34,10 @@
|
|||
jsonpointer "^5.0.0"
|
||||
leven "^3.1.0"
|
||||
|
||||
"@atproto/api@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.4.tgz#b73446f2344783c42c6040082756449443f15750"
|
||||
integrity sha512-9gwZt4v4pngfD4mgsET9i9Ym0PpMSzftTzqBjCbFpObx15zMkFemYnLUnyT/NEww2u/aRxjAe2TeBnU0dIbbuQ==
|
||||
"@atproto/api@^0.10.5":
|
||||
version "0.10.5"
|
||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.10.5.tgz#e778e2843d08690df8df81f24028a7578e9b3cb4"
|
||||
integrity sha512-GYdST5sPKU2JnPmm8x3KqjOSlDiYXrp4GkW7bpQTVLPabnUNq5NLN6HJEoJABjjOAsaLF12rBoV+JpRb1UjNsQ==
|
||||
dependencies:
|
||||
"@atproto/common-web" "^0.2.3"
|
||||
"@atproto/lexicon" "^0.3.2"
|
||||
|
@ -3115,6 +3115,27 @@
|
|||
xcode "^3.0.1"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/config-plugins@~5.0.3":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-5.0.4.tgz#216fea6558fe66615af1370de55193f4181cb23e"
|
||||
integrity sha512-vzUcVpqOMs3h+hyRdhGwk+eGIOhXa5xYdd92yO17RMNHav3v/+ekMbs7XA2c3lepMO8Yd4/5hqmRw9ZTL6jGzg==
|
||||
dependencies:
|
||||
"@expo/config-types" "^47.0.0"
|
||||
"@expo/json-file" "8.2.36"
|
||||
"@expo/plist" "0.0.18"
|
||||
"@expo/sdk-runtime-versions" "^1.0.0"
|
||||
"@react-native/normalize-color" "^2.0.0"
|
||||
chalk "^4.1.2"
|
||||
debug "^4.3.1"
|
||||
find-up "~5.0.0"
|
||||
getenv "^1.0.0"
|
||||
glob "7.1.6"
|
||||
resolve-from "^5.0.0"
|
||||
semver "^7.3.5"
|
||||
slash "^3.0.0"
|
||||
xcode "^3.0.1"
|
||||
xml2js "0.4.23"
|
||||
|
||||
"@expo/config-plugins@~7.8.2":
|
||||
version "7.8.2"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-7.8.2.tgz#c00ce93c4d6c2cb9e345ed9cd56ceeea05ab8ddb"
|
||||
|
@ -3138,6 +3159,11 @@
|
|||
xcode "^3.0.1"
|
||||
xml2js "0.6.0"
|
||||
|
||||
"@expo/config-types@^47.0.0":
|
||||
version "47.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d"
|
||||
integrity sha512-r0pWfuhkv7KIcXMUiNACJmJKKwlTBGMw9VZHNdppS8/0Nve8HZMTkNRFQzTHW1uH3pBj8jEXpyw/2vSWDHex9g==
|
||||
|
||||
"@expo/config-types@^50.0.0", "@expo/config-types@^50.0.0-alpha.1":
|
||||
version "50.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-50.0.0.tgz#b534d3ec997ec60f8af24f6ad56244c8afc71a0b"
|
||||
|
@ -3160,6 +3186,23 @@
|
|||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/config@~7.0.0":
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-7.0.3.tgz#c9c634e76186de25e296485e51418f1e52966e6e"
|
||||
integrity sha512-joVtB5o+NF40Tmsdp65UzryRtbnCuMbXkVO4wJnNJO4aaK0EYLdHCYSewORVqNcDfGN0LphQr8VTG2npbd9CJA==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
"@expo/config-plugins" "~5.0.3"
|
||||
"@expo/config-types" "^47.0.0"
|
||||
"@expo/json-file" "8.2.36"
|
||||
getenv "^1.0.0"
|
||||
glob "7.1.6"
|
||||
require-from-string "^2.0.2"
|
||||
resolve-from "^5.0.0"
|
||||
semver "7.3.2"
|
||||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/config@~8.5.0":
|
||||
version "8.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/config/-/config-8.5.0.tgz#c618e016c3272335e33fec02fb7fd67e4dcc3342"
|
||||
|
@ -3259,6 +3302,15 @@
|
|||
semver "7.3.2"
|
||||
tempy "0.3.0"
|
||||
|
||||
"@expo/json-file@8.2.36":
|
||||
version "8.2.36"
|
||||
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.36.tgz#62a505cb7f30a34d097386476794680a3f7385ff"
|
||||
integrity sha512-tOZfTiIFA5KmMpdW9KF7bc6CFiGjb0xnbieJhTGlHrLL+ps2G0OkqmuZ3pFEXBOMnJYUVpnSy++52LFxvpa5ZQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "~7.10.4"
|
||||
json5 "^1.0.1"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/json-file@^8.2.37":
|
||||
version "8.2.37"
|
||||
resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-8.2.37.tgz#9c02d3b42134907c69cc0a027b18671b69344049"
|
||||
|
@ -3328,6 +3380,15 @@
|
|||
split "^1.0.1"
|
||||
sudo-prompt "9.1.1"
|
||||
|
||||
"@expo/plist@0.0.18":
|
||||
version "0.0.18"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.0.18.tgz#9abcde78df703a88f6d9fa1a557ee2f045d178b0"
|
||||
integrity sha512-+48gRqUiz65R21CZ/IXa7RNBXgAI/uPSdvJqoN9x1hfL44DNbUoWHgHiEXTx7XelcATpDwNTz6sHLfy0iNqf+w==
|
||||
dependencies:
|
||||
"@xmldom/xmldom" "~0.7.0"
|
||||
base64-js "^1.2.3"
|
||||
xmlbuilder "^14.0.0"
|
||||
|
||||
"@expo/plist@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.1.0.tgz#eabc95f951d14e10c87fd0443ee01d567371f058"
|
||||
|
@ -4467,6 +4528,18 @@
|
|||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||
|
||||
"@radix-ui/react-dismissable-layer@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4"
|
||||
integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-escape-keydown" "1.0.3"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.0.1":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.5.tgz#19bf4de8ffa348b4eb6a86842f14eff93d741170"
|
||||
|
@ -4481,6 +4554,20 @@
|
|||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-dropdown-menu@^2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.6.tgz#cdf13c956c5e263afe4e5f3587b3071a25755b63"
|
||||
integrity sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-menu" "2.0.6"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
|
||||
"@radix-ui/react-focus-guards@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad"
|
||||
|
@ -4498,6 +4585,16 @@
|
|||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
|
||||
"@radix-ui/react-focus-scope@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525"
|
||||
integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
|
||||
"@radix-ui/react-id@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0"
|
||||
|
@ -4531,6 +4628,31 @@
|
|||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-menu@2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e"
|
||||
integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-collection" "1.0.3"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-direction" "1.0.1"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.5"
|
||||
"@radix-ui/react-focus-guards" "1.0.1"
|
||||
"@radix-ui/react-focus-scope" "1.0.4"
|
||||
"@radix-ui/react-id" "1.0.1"
|
||||
"@radix-ui/react-popper" "1.1.3"
|
||||
"@radix-ui/react-portal" "1.0.4"
|
||||
"@radix-ui/react-presence" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-roving-focus" "1.0.4"
|
||||
"@radix-ui/react-slot" "1.0.2"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-popper@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
|
||||
|
@ -4548,6 +4670,23 @@
|
|||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/rect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-popper@1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42"
|
||||
integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@floating-ui/react-dom" "^2.0.0"
|
||||
"@radix-ui/react-arrow" "1.0.3"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.1"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.1"
|
||||
"@radix-ui/react-use-rect" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
"@radix-ui/rect" "1.0.1"
|
||||
|
||||
"@radix-ui/react-portal@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.3.tgz#ffb961244c8ed1b46f039e6c215a6c4d9989bda1"
|
||||
|
@ -4556,6 +4695,14 @@
|
|||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-portal@1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15"
|
||||
integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
|
||||
"@radix-ui/react-presence@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba"
|
||||
|
@ -4664,6 +4811,13 @@
|
|||
dependencies:
|
||||
merge-options "^3.0.4"
|
||||
|
||||
"@react-native-async-storage/async-storage@^1.15.2":
|
||||
version "1.22.0"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.22.0.tgz#202a9afd15a5b829c39b709d0ca3942612441efc"
|
||||
integrity sha512-b5KD010iiZnot86RbAaHpLuHwmPW2qA3SSN/OSZhd1kBoINEQEVBuv+uFtcaTxAhX27bT0wd13GOb2IOSDUXSA==
|
||||
dependencies:
|
||||
merge-options "^3.0.4"
|
||||
|
||||
"@react-native-camera-roll/camera-roll@^5.2.2":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-camera-roll/camera-roll/-/camera-roll-5.7.2.tgz#db11525ae26c8a61630c424aebd323a7c784a921"
|
||||
|
@ -4832,13 +4986,6 @@
|
|||
prompts "^2.4.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":
|
||||
version "3.2.0"
|
||||
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"
|
||||
integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==
|
||||
|
||||
"@xmldom/xmldom@~0.7.7":
|
||||
"@xmldom/xmldom@~0.7.0", "@xmldom/xmldom@~0.7.7":
|
||||
version "0.7.13"
|
||||
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.13.tgz#ff34942667a4e19a9f4a0996a76814daac364cf3"
|
||||
integrity sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==
|
||||
|
@ -11651,6 +11798,14 @@ expo-camera@~14.0.1:
|
|||
dependencies:
|
||||
invariant "^2.2.4"
|
||||
|
||||
expo-constants@^13.0.2:
|
||||
version "13.2.4"
|
||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-13.2.4.tgz#eab4a553f074b2c60ad7a158d3b82e3484a94606"
|
||||
integrity sha512-Zobau8EuTk2GgafwkfGnWM6CmSLB7X8qnQXVuXe0nd3v92hfQUmRWGhJwH88uxXj3LrfqctM6PaJ8taG1vxfBw==
|
||||
dependencies:
|
||||
"@expo/config" "~7.0.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
expo-constants@~15.4.0:
|
||||
version "15.4.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-15.4.1.tgz#f76f347cf687b6630e1e3b9a385a4e42771671a4"
|
||||
|
@ -11700,6 +11855,13 @@ expo-dev-menu@4.5.3:
|
|||
expo-dev-menu-interface "1.7.2"
|
||||
semver "^7.5.3"
|
||||
|
||||
expo-device@~4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-4.1.1.tgz#5de94144113ffb7fa0f37fa3d70e452113954c10"
|
||||
integrity sha512-It0SGtKcvzQSf+Co6zdPdB63zZvG2/rDolB1lqswMNKj03Y7KVU41s5tcQCqNczj7tmeN3CJy7A8YhYGKdb7gA==
|
||||
dependencies:
|
||||
ua-parser-js "^0.7.19"
|
||||
|
||||
expo-device@~5.9.2:
|
||||
version "5.9.2"
|
||||
resolved "https://registry.yarnpkg.com/expo-device/-/expo-device-5.9.2.tgz#697e96f52d213a141b6f265f1e274e9d5e98c92c"
|
||||
|
@ -14892,6 +15054,16 @@ js-queue@2.0.2:
|
|||
dependencies:
|
||||
easy-stack "^1.0.1"
|
||||
|
||||
js-sha256@^0.10.1:
|
||||
version "0.10.1"
|
||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.10.1.tgz#b40104ba1368e823fdd5f41b66b104b15a0da60d"
|
||||
integrity sha512-5obBtsz9301ULlsgggLg542s/jqtddfOpV5KJc4hajc9JV8GeY2gZHSVpYBn4nWqAUTJ9v+xwtbJ1mIBgIH5Vw==
|
||||
|
||||
js-sha256@^0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.11.0.tgz#256a921d9292f7fe98905face82e367abaca9576"
|
||||
integrity sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==
|
||||
|
||||
js-sha256@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
|
||||
|
@ -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"
|
||||
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
|
||||
|
||||
json5@^1.0.2:
|
||||
json5@^1.0.1, json5@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593"
|
||||
integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==
|
||||
|
@ -18372,11 +18544,23 @@ react-is@^17.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-keyed-flatten-children@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-keyed-flatten-children/-/react-keyed-flatten-children-3.0.0.tgz#b6ad0bde437d3ab86c8af3a1902d164be2a29d67"
|
||||
integrity sha512-tSH6gvOyQjt3qtjG+kU9sTypclL1672yjpVufcE3aHNM0FhvjBUQZqsb/awIux4zEuVC3k/DP4p0GdTT/QUt/Q==
|
||||
dependencies:
|
||||
react-is "^18.2.0"
|
||||
|
||||
react-native-appstate-hook@^1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
|
||||
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:
|
||||
version "3.4.9"
|
||||
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"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-native-get-random-values@^1.6.0:
|
||||
version "1.10.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.10.0.tgz#c2c5f12a4ef8b1175145347b4a4b9f9a40d9ffc8"
|
||||
integrity sha512-gZ1zbXhbb8+Jy9qYTV8c4Nf45/VB4g1jmXuavY5rPfUn7x3ok9Vl3FTl0dnE92Z4FFtfbUNNwtSfcmomdtWg+A==
|
||||
dependencies:
|
||||
fast-base64-decode "^1.0.0"
|
||||
|
||||
react-native-get-random-values@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-get-random-values/-/react-native-get-random-values-1.8.0.tgz#1cb4bd4bd3966a356e59697b8f372999fe97cb16"
|
||||
|
@ -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"
|
||||
integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==
|
||||
|
||||
statsig-js@4.45.1:
|
||||
version "4.45.1"
|
||||
resolved "https://registry.yarnpkg.com/statsig-js/-/statsig-js-4.45.1.tgz#b1f5b9c52adc4a8aece376fb011416c89227932f"
|
||||
integrity sha512-h94RzFQsJCQCNwQXpZ9OBXcvCxDnkXF6OrCekd81ySvY2l4JSowpxMWX3Iw6IDFzfTfKdER9JQzFLhMSQbT+YQ==
|
||||
dependencies:
|
||||
js-sha256 "^0.10.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
statsig-js@4.49.0:
|
||||
version "4.49.0"
|
||||
resolved "https://registry.yarnpkg.com/statsig-js/-/statsig-js-4.49.0.tgz#8470a9ac218a93d36f4b7b306ff9377e48064740"
|
||||
integrity sha512-N4drx6fzI168Q4NndFY3IJbSDqpWSBWvS290H/RnT/g3Et58SvtXzG5qqgzmqy4CwcmwH+IL8K15pL7hPnfvUQ==
|
||||
dependencies:
|
||||
js-sha256 "^0.11.0"
|
||||
uuid "^8.3.2"
|
||||
|
||||
statsig-react-native-expo@^4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/statsig-react-native-expo/-/statsig-react-native-expo-4.6.1.tgz#0bdf49fee7112f7f28bff2405f4ba0c1727bb3d6"
|
||||
integrity sha512-rB60c+WSrQPmjW9j75d+acUtwSOe38PE2KTDHiOv1Mf+0TCcFtGYlJmKCibWvbeXR7ZAyjjGeroh23bCSEZauQ==
|
||||
dependencies:
|
||||
"@react-native-async-storage/async-storage" "^1.15.2"
|
||||
expo-constants "^13.0.2"
|
||||
expo-device "~4.1.1"
|
||||
js-sha256 "^0.9.0"
|
||||
react-native-get-random-values "^1.6.0"
|
||||
statsig-react "^1.21.1"
|
||||
uuid "^8.3.2"
|
||||
|
||||
statsig-react@^1.21.1:
|
||||
version "1.35.0"
|
||||
resolved "https://registry.yarnpkg.com/statsig-react/-/statsig-react-1.35.0.tgz#ad5730b83f564c640623e954fcbcbe848e939946"
|
||||
integrity sha512-KLN7dhq6FvAl25Z0QN6IINFBgM3yn0GMafoE698tYZqRf911xvevFaR7qUXiTz3W9vmFYrmFRouqVMfCv7DW0A==
|
||||
dependencies:
|
||||
statsig-js "4.45.1"
|
||||
|
||||
statsig-react@^1.36.0:
|
||||
version "1.36.0"
|
||||
resolved "https://registry.yarnpkg.com/statsig-react/-/statsig-react-1.36.0.tgz#c2171268a6c76eee534849ec9556b836baba04b6"
|
||||
integrity sha512-QcTHla3ypfn2RvrnHGNlqWbiC2W/ZjcMM5LT6ExNV4skH7Xhspto3dMS3JVzBhOb74OEDZK4DbxQj9Wdz6XW0w==
|
||||
dependencies:
|
||||
statsig-js "4.49.0"
|
||||
|
||||
statuses@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
|
@ -20827,6 +21061,11 @@ typescript@^5.3.3:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
ua-parser-js@^0.7.19:
|
||||
version "0.7.37"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.37.tgz#e464e66dac2d33a7a1251d7d7a99d6157ec27832"
|
||||
integrity sha512-xV8kqRKM+jhMvcHWUKthV9fNebIzrNy//2O9ZwWcfiBFR5f25XVZPLlEajk/sf3Ra15V92isyQqnIEXRDaZWEA==
|
||||
|
||||
ua-parser-js@^0.7.33:
|
||||
version "0.7.35"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.35.tgz#8bda4827be4f0b1dda91699a29499575a1f1d307"
|
||||
|
@ -21108,7 +21347,7 @@ utils-merge@1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@^3.0.1:
|
||||
uuid@^3.0.1, uuid@^3.3.2:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
@ -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"
|
||||
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
|
||||
|
||||
xml2js@0.4.23:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
|
||||
dependencies:
|
||||
sax ">=0.6.0"
|
||||
xmlbuilder "~11.0.0"
|
||||
|
||||
xml2js@0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.0.tgz#07afc447a97d2bd6507a1f76eeadddb09f7a8282"
|
||||
|
|
Loading…
Reference in New Issue