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