Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35414b82c1 | |||
| 31a74f8d31 | |||
| 0d4367833b | |||
| a2005b3f27 | |||
| d6610eddd4 | |||
| 744e141703 | |||
| 30c2394271 | |||
| 4aac5c1482 | |||
| fa9e42c2fd | |||
| fa52da8628 | |||
| 60d826c4f0 | |||
| c3bb121250 | |||
| 3b4c5becda | |||
| 4278171dd7 | |||
| d1a356ceec | |||
| 5b3a47ed87 | |||
| e326694c1f | |||
| 2682989f3c | |||
| b803098228 | |||
| db57b5d939 | |||
| 82a1cbebdf | |||
| 371e31bd93 | |||
| 749d4c5bd4 | |||
| 7861de140c | |||
| 3a666db699 | |||
| 9833afcd1b | |||
| 73f6d878bd | |||
| 91b6d32c5d | |||
| 9847f7338e | |||
| a99fae861e | |||
| be56364733 | |||
| 4c4644d089 | |||
| e61f1632c3 | |||
| 0a2e459529 | |||
| 992b9ebaff | |||
| 97c5cfa9b7 | |||
| 5f98594eff | |||
| 735a9c1e3e | |||
| 01194dd32d | |||
| 1e57665aee | |||
| 6df3dc0812 | |||
| 9c6369b4ec | |||
| 5dd417cf3f |
@@ -53,12 +53,12 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.4'
|
||||
xcode-version: "16.4"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
version: 1.16.2
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
|
||||
@@ -192,12 +192,12 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
xcode-version: "16.2"
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
with:
|
||||
version: 1.14.3
|
||||
version: 1.16.2
|
||||
|
||||
- name: 💾 Cache Pods
|
||||
uses: actions/cache@v3
|
||||
@@ -293,8 +293,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
distribution: "temurin"
|
||||
java-version: "17"
|
||||
|
||||
- name: ⚙️ Install dependencies
|
||||
run: yarn install
|
||||
@@ -332,7 +332,7 @@ jobs:
|
||||
id: timestamp
|
||||
uses: nanzm/get-time-action@master
|
||||
with:
|
||||
format: 'MM-DD-HH-mm-ss'
|
||||
format: "MM-DD-HH-mm-ss"
|
||||
|
||||
- name: 🚀 Upload Artifact
|
||||
id: upload-artifact
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
jest.mock('sentry-expo', () => ({
|
||||
init: () => jest.fn(),
|
||||
}))
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||
import {createDownloadResumable, deleteAsync} from 'expo-file-system/legacy'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
|
||||
import {
|
||||
|
||||
+2
-12
@@ -28,8 +28,6 @@ module.exports = function (_config) {
|
||||
|
||||
const UPDATES_ENABLED = IS_TESTFLIGHT || IS_PRODUCTION
|
||||
|
||||
const USE_SENTRY = Boolean(process.env.SENTRY_AUTH_TOKEN)
|
||||
|
||||
return {
|
||||
expo: {
|
||||
version: VERSION,
|
||||
@@ -43,6 +41,7 @@ module.exports = function (_config) {
|
||||
icon: './assets/app-icons/ios_icon_default_light.png',
|
||||
userInterfaceStyle: 'automatic',
|
||||
primaryColor: '#1083fe',
|
||||
newArchEnabled: false,
|
||||
ios: {
|
||||
supportsTablet: false,
|
||||
bundleIdentifier: 'xyz.blueskyweb.app',
|
||||
@@ -194,26 +193,17 @@ module.exports = function (_config) {
|
||||
'react-native-edge-to-edge',
|
||||
{android: {enforceNavigationBarContrast: false}},
|
||||
],
|
||||
USE_SENTRY && [
|
||||
'@sentry/react-native/expo',
|
||||
{
|
||||
organization: 'blueskyweb',
|
||||
project: 'app',
|
||||
url: 'https://sentry.io',
|
||||
},
|
||||
],
|
||||
[
|
||||
'expo-build-properties',
|
||||
{
|
||||
ios: {
|
||||
deploymentTarget: '15.1',
|
||||
newArchEnabled: false,
|
||||
buildReactNativeFromSource: true,
|
||||
},
|
||||
android: {
|
||||
compileSdkVersion: 35,
|
||||
targetSdkVersion: 35,
|
||||
buildToolsVersion: '35.0.0',
|
||||
newArchEnabled: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
+1
-2
@@ -63,7 +63,7 @@ After you do `yarn ios` and `yarn android` once, you can later just run `yarn we
|
||||
### Tips
|
||||
|
||||
- Copy the `.env.example` to `.env` and fill in any necessary tokens. (The Sentry token is NOT required; see instructions below if you want to enable Sentry.)
|
||||
- To run on the device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release`.
|
||||
- To run on the device, add `--device` to the command (e.g. `yarn android --device`). To build in production mode (slower build, faster app), also add `--variant release` on Android or `--configuration Release` on iOS.
|
||||
- If you want to use Expo EAS on your own builds without ejecting from Expo, make sure to change the `owner` and `extra.eas.projectId` properties. If you do not have an Expo account, you may remove these properties.
|
||||
- `npx react-native info` Checks what has been installed.
|
||||
- If the Android simulator frequently hangs or is very sluggish, [bump its memory limit](https://stackoverflow.com/a/40068396)
|
||||
@@ -164,7 +164,6 @@ See [testing.md](./testing.md).
|
||||
- TextEncoder / TextDecoder
|
||||
- react-native-url-polyfill
|
||||
- Array#findLast (on web)
|
||||
- atob (on native)
|
||||
|
||||
### Sentry sourcemaps
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"build": {
|
||||
"base": {
|
||||
"node": "18.18.2"
|
||||
"node": "20.19.4"
|
||||
},
|
||||
"development": {
|
||||
"extends": "base",
|
||||
|
||||
+1
-12
@@ -33,7 +33,7 @@ jest.mock('react-native-safe-area-context', () => {
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('expo-file-system', () => ({
|
||||
jest.mock('expo-file-system/legacy', () => ({
|
||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||
deleteAsync: jest.fn(),
|
||||
createDownloadResumable: jest.fn(),
|
||||
@@ -65,17 +65,6 @@ jest.mock('lande', () => ({
|
||||
default: jest.fn().mockReturnValue([['eng']]),
|
||||
}))
|
||||
|
||||
jest.mock('sentry-expo', () => ({
|
||||
init: () => jest.fn(),
|
||||
Native: {
|
||||
ReactNativeTracing: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
})),
|
||||
ReactNavigationInstrumentation: jest.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('crypto', () => ({}))
|
||||
|
||||
jest.mock('expo-application', () => ({
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
// Learn more https://docs.expo.io/guides/customizing-metro
|
||||
const {getSentryExpoConfig} = require('@sentry/react-native/metro')
|
||||
const cfg = getSentryExpoConfig(__dirname)
|
||||
const {getDefaultConfig} = require('expo/metro-config')
|
||||
|
||||
const cfg = getDefaultConfig(__dirname)
|
||||
|
||||
cfg.resolver.sourceExts = process.env.RN_SRC_EXT
|
||||
? process.env.RN_SRC_EXT.split(',').concat(cfg.resolver.sourceExts)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ExpoModulesCore
|
||||
import React
|
||||
import UIKit
|
||||
|
||||
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ExpoModulesCore
|
||||
import React
|
||||
|
||||
// This view will be used as a native component. Make sure to inherit from `ExpoView`
|
||||
// to apply the proper styling (e.g. border radius and shadows).
|
||||
|
||||
+57
-65
@@ -63,8 +63,7 @@
|
||||
"intl:push": "crowdin push translations --verbose -b main",
|
||||
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android",
|
||||
"update-extensions": "bash scripts/updateExtensions.sh",
|
||||
"export": "npx expo export --dump-sourcemap && yarn upload-native-sourcemaps",
|
||||
"upload-native-sourcemaps": "npx sentry-expo-upload-sourcemaps dist",
|
||||
"export": "npx expo export --dump-sourcemap",
|
||||
"make-deploy-bundle": "bash scripts/bundleUpdate.sh",
|
||||
"generate-webpack-stats-file": "EXPO_PUBLIC_GENERATE_STATS=1 yarn build-web",
|
||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web",
|
||||
@@ -92,15 +91,12 @@
|
||||
"@lingui/react": "^4.14.1",
|
||||
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||
"@miblanchard/react-native-slider": "^2.6.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.7.1",
|
||||
"@react-native-async-storage/async-storage": "2.1.2",
|
||||
"@react-native-menu/menu": "^1.2.3",
|
||||
"@react-native-picker/picker": "2.11.0",
|
||||
"@mozzius/expo-dynamic-app-icon": "^1.8.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-navigation/bottom-tabs": "^7.3.13",
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
"@react-navigation/native": "^7.1.9",
|
||||
"@react-navigation/native-stack": "^7.3.13",
|
||||
"@sentry/react-native": "~6.14.0",
|
||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||
"@tanstack/react-query": "^5.8.1",
|
||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||
@@ -126,41 +122,40 @@
|
||||
"bcp-47": "^2.1.0",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"deprecated-react-native-prop-types": "^5.0.0",
|
||||
"email-validator": "^2.0.4",
|
||||
"emoji-mart": "^5.5.2",
|
||||
"emoji-regex": "^10.4.0",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"expo": "53.0.11",
|
||||
"expo-application": "~6.1.4",
|
||||
"expo-blur": "~14.1.5",
|
||||
"expo-build-properties": "~0.14.6",
|
||||
"expo-camera": "~16.1.8",
|
||||
"expo-clipboard": "~7.1.4",
|
||||
"expo-dev-client": "~5.2.0",
|
||||
"expo-device": "~7.1.4",
|
||||
"expo-file-system": "~18.1.10",
|
||||
"expo-font": "~13.3.1",
|
||||
"expo-haptics": "~14.1.4",
|
||||
"expo-image": "^2.4.0",
|
||||
"expo": "54.0.2",
|
||||
"expo-application": "~7.0.7",
|
||||
"expo-blur": "~15.0.7",
|
||||
"expo-build-properties": "~1.0.8",
|
||||
"expo-camera": "~17.0.7",
|
||||
"expo-clipboard": "~8.0.7",
|
||||
"expo-dev-client": "~6.0.12",
|
||||
"expo-device": "~8.0.7",
|
||||
"expo-file-system": "~19.0.12",
|
||||
"expo-font": "~14.0.8",
|
||||
"expo-haptics": "~15.0.7",
|
||||
"expo-image": "~3.0.8",
|
||||
"expo-image-crop-tool": "^0.1.8",
|
||||
"expo-image-manipulator": "~13.1.7",
|
||||
"expo-image-picker": "17.0.5",
|
||||
"expo-intent-launcher": "^12.1.5",
|
||||
"expo-linear-gradient": "~14.1.5",
|
||||
"expo-linking": "~7.1.5",
|
||||
"expo-localization": "~16.1.5",
|
||||
"expo-location": "~18.1.6",
|
||||
"expo-media-library": "~17.1.7",
|
||||
"expo-notifications": "~0.31.3",
|
||||
"expo-screen-orientation": "~8.1.7",
|
||||
"expo-sharing": "~13.1.5",
|
||||
"expo-splash-screen": "~0.30.9",
|
||||
"expo-system-ui": "~5.0.8",
|
||||
"expo-task-manager": "~13.1.5",
|
||||
"expo-updates": "~0.28.14",
|
||||
"expo-video": "~2.2.1",
|
||||
"expo-web-browser": "~14.1.6",
|
||||
"expo-image-manipulator": "~14.0.7",
|
||||
"expo-image-picker": "~17.0.8",
|
||||
"expo-intent-launcher": "~13.0.7",
|
||||
"expo-linear-gradient": "~15.0.7",
|
||||
"expo-linking": "~8.0.8",
|
||||
"expo-localization": "~17.0.7",
|
||||
"expo-location": "~19.0.7",
|
||||
"expo-media-library": "~18.1.1",
|
||||
"expo-notifications": "~0.32.11",
|
||||
"expo-screen-orientation": "~9.0.7",
|
||||
"expo-sharing": "~14.0.7",
|
||||
"expo-splash-screen": "~31.0.9",
|
||||
"expo-system-ui": "~6.0.7",
|
||||
"expo-task-manager": "~14.0.7",
|
||||
"expo-updates": "~29.0.10",
|
||||
"expo-video": "~3.0.11",
|
||||
"expo-web-browser": "~15.0.7",
|
||||
"fast-text-encoding": "^1.0.6",
|
||||
"history": "^5.3.0",
|
||||
"hls.js": "^1.6.2",
|
||||
@@ -178,37 +173,36 @@
|
||||
"patch-package": "^6.5.1",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"psl": "^1.9.0",
|
||||
"radix-ui": "^1.2.0",
|
||||
"react": "19.0.0",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "19.1.0",
|
||||
"react-compiler-runtime": "^19.1.0-rc.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-image-crop": "^11.0.7",
|
||||
"react-is": "19",
|
||||
"react-keyed-flatten-children": "^5.0.0",
|
||||
"react-native": "^0.79.3",
|
||||
"react-native-compressor": "^1.11.0",
|
||||
"react-native-date-picker": "^5.0.12",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-compressor": "^1.12.0",
|
||||
"react-native-date-picker": "^5.0.13",
|
||||
"react-native-device-attest": "^0.1.6",
|
||||
"react-native-drawer-layout": "^4.1.8",
|
||||
"react-native-edge-to-edge": "^1.6.0",
|
||||
"react-native-gesture-handler": "2.25.0",
|
||||
"react-native-gesture-handler": "~2.28.0",
|
||||
"react-native-get-random-values": "~1.11.0",
|
||||
"react-native-ios-context-menu": "^1.15.3",
|
||||
"react-native-keyboard-controller": "^1.17.5",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-mmkv": "^2.12.2",
|
||||
"react-native-pager-view": "6.8.0",
|
||||
"react-native-progress": "bluesky-social/react-native-progress",
|
||||
"react-native-qrcode-styled": "^0.3.3",
|
||||
"react-native-reanimated": "~3.17.5",
|
||||
"react-native-reanimated": "^3.19.1",
|
||||
"react-native-root-siblings": "^5.0.1",
|
||||
"react-native-safe-area-context": "5.4.0",
|
||||
"react-native-screens": "^4.11.1",
|
||||
"react-native-svg": "15.12.0",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
"react-native-screens": "~4.16.0",
|
||||
"react-native-svg": "15.12.1",
|
||||
"react-native-uitextview": "^1.4.0",
|
||||
"react-native-url-polyfill": "^1.3.0",
|
||||
"react-native-uuid": "^2.0.3",
|
||||
"react-native-view-shot": "^4.0.3",
|
||||
"react-native-web": "~0.20.0",
|
||||
"react-native-web": "^0.21.0",
|
||||
"react-native-web-webview": "^1.0.2",
|
||||
"react-native-webview": "^13.13.5",
|
||||
"react-remove-scroll-bar": "^2.3.8",
|
||||
@@ -220,7 +214,6 @@
|
||||
"tippy.js": "^6.3.7",
|
||||
"tlds": "^1.234.0",
|
||||
"tldts": "^6.1.46",
|
||||
"zeego": "^1.6.2",
|
||||
"zod": "^3.20.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -228,14 +221,13 @@
|
||||
"@babel/core": "^7.26.0",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"@babel/runtime": "^7.26.0",
|
||||
"@expo/config-plugins": "~10.0.2",
|
||||
"@expo/config-plugins": "~54.0.1",
|
||||
"@lingui/cli": "^4.14.1",
|
||||
"@lingui/macro": "^4.14.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/eslint-config": "^0.79.3",
|
||||
"@react-native/typescript-config": "^0.79.3",
|
||||
"@sentry/webpack-plugin": "^3.2.2",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/eslint-config": "^0.81.4",
|
||||
"@react-native/typescript-config": "^0.81.4",
|
||||
"@testing-library/jest-native": "^5.4.3",
|
||||
"@testing-library/react-native": "^13.2.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
@@ -245,14 +237,14 @@
|
||||
"@types/lodash.shuffle": "^4.2.7",
|
||||
"@types/psl": "^1.1.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"babel-jest": "^29.7.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-module-resolver": "^5.0.2",
|
||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||
"babel-preset-expo": "~13.1.11",
|
||||
"babel-preset-expo": "~54.0.0",
|
||||
"eslint": "^8.19.0",
|
||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||
"eslint-plugin-ft-flow": "^2.0.3",
|
||||
@@ -266,7 +258,7 @@
|
||||
"husky": "^8.0.3",
|
||||
"is-ci": "^3.0.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest-expo": "~53.0.7",
|
||||
"jest-expo": "~54.0.10",
|
||||
"jest-junit": "^16.0.0",
|
||||
"lint-staged": "^13.2.3",
|
||||
"lockfile-lint": "^4.14.0",
|
||||
@@ -276,14 +268,14 @@
|
||||
"react-refresh": "^0.14.0",
|
||||
"svgo": "^3.3.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript": "^5.9.2",
|
||||
"webpack-bundle-analyzer": "^4.10.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@expo/image-utils": "0.6.3",
|
||||
"@react-native/babel-preset": "0.79.3",
|
||||
"@react-native/normalize-colors": "0.79.3",
|
||||
"**/expo-constants": "17.0.3",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/normalize-colors": "0.81.4",
|
||||
"**/@expo/image-utils": "0.8.7",
|
||||
"**/expo-constants": "18.0.8",
|
||||
"**/expo-device": "7.1.4",
|
||||
"**/zod": "3.23.8",
|
||||
"**/multiformats": "9.9.0"
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
index 4ed2307..ede1181 100644
|
||||
--- a/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
+++ b/node_modules/@mattermost/react-native-paste-input/android/src/main/java/com/mattermost/pasteinputtext/PasteTextInputManager.kt
|
||||
@@ -54,7 +54,7 @@ class PasteTextInputManager(context: ReactApplicationContext) : ReactTextInputMa
|
||||
}
|
||||
|
||||
override fun getExportedCustomBubblingEventTypeConstants(): MutableMap<String, Any> {
|
||||
- val map = super.getExportedCustomBubblingEventTypeConstants()!!
|
||||
+ val map = super.getExportedCustomBubblingEventTypeConstants().toMutableMap()
|
||||
map["onPaste"] = MapBuilder.of(
|
||||
"phasedRegistrationNames",
|
||||
MapBuilder.of("bubbled", "onPaste")
|
||||
@@ -1,13 +0,0 @@
|
||||
diff --git a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
index c282ade..8777755 100755
|
||||
--- a/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
+++ b/node_modules/@sentry/react-native/scripts/expo-upload-sourcemaps.js
|
||||
@@ -215,7 +215,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) {
|
||||
|
||||
const isHermes = assets.find(asset => asset.endsWith('.hbc'));
|
||||
const windowsCallback = process.platform === "win32" ? 'node ' : '';
|
||||
- execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, {
|
||||
+ execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')} --dist ${process.env.SENTRY_DIST}`, {
|
||||
env: {
|
||||
...process.env,
|
||||
[SENTRY_PROJECT]: sentryProject,
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
index f1255e8..a9b49e5 100644
|
||||
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
@@ -112,11 +112,9 @@ class MediaLibraryModule : Module() {
|
||||
}
|
||||
|
||||
AsyncFunction("createAssetAsync") { localUri: String, albumId: String?, promise: Promise ->
|
||||
- throwUnlessPermissionsGranted {
|
||||
- withModuleScope(promise) {
|
||||
- CreateAssetWithAlbumId(context, localUri, promise, true, albumId)
|
||||
- .execute()
|
||||
- }
|
||||
+ withModuleScope(promise) {
|
||||
+ CreateAssetWithAlbumId(context, localUri, promise, true, albumId)
|
||||
+ .execute()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
diff --git a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
index 2dc1db2..ee844e8 100644
|
||||
--- a/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
+++ b/node_modules/expo-media-library/android/src/main/java/expo/modules/medialibrary/MediaLibraryModule.kt
|
||||
@@ -105,7 +105,7 @@ class MediaLibraryModule : Module() {
|
||||
}
|
||||
|
||||
AsyncFunction("createAssetAsync") Coroutine { localUri: String, albumId: String? ->
|
||||
- requireSystemPermissions()
|
||||
+ // requireSystemPermissions()
|
||||
return@Coroutine createAssetWithAlbumId(context, localUri, true, albumId)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/Scro
|
||||
index d029337..0f63ea3 100644
|
||||
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||
@@ -1003,6 +1003,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
@@ -1038,6 +1038,11 @@ - (void)_adjustForMaintainVisibleContentPosition
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
diff --git a/node_modules/react-native-date-picker/ios/RNDatePicker.h b/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
index 480746e..470dc3a 100644
|
||||
--- a/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
+++ b/node_modules/react-native-date-picker/ios/RNDatePicker.h
|
||||
@@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_END
|
||||
#else
|
||||
#import "DatePicker.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
+#include <string>
|
||||
|
||||
@interface RNDatePicker : DatePicker
|
||||
|
||||
@@ -22,4 +23,3 @@ NS_ASSUME_NONNULL_END
|
||||
@end
|
||||
|
||||
#endif
|
||||
-
|
||||
@@ -1,36 +0,0 @@
|
||||
diff --git a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
index 43d11b4..5bf0d32 100644
|
||||
--- a/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
+++ b/node_modules/react-native-gesture-handler/apple/RNGestureHandler.mm
|
||||
@@ -461,16 +461,23 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
|
||||
shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
|
||||
{
|
||||
- if ([_handlersToWaitFor count]) {
|
||||
- RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
- if (handler != nil) {
|
||||
- for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
- if ([handler.tag isEqual:handlerTag]) {
|
||||
- return YES;
|
||||
- }
|
||||
- }
|
||||
+ RNGestureHandler *handler = [RNGestureHandler findGestureHandlerByRecognizer:otherGestureRecognizer];
|
||||
+ if (handler == nil) {
|
||||
+ return NO;
|
||||
+ }
|
||||
+
|
||||
+ for (NSNumber *handlerTag in _handlersToWaitFor) {
|
||||
+ if ([handler.tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
}
|
||||
}
|
||||
+
|
||||
+ for (NSNumber *handlerTag in handler->_handlersThatShouldWait) {
|
||||
+ if ([_tag isEqual:handlerTag]) {
|
||||
+ return YES;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
return NO;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
diff --git a/node_modules/react-native-mmkv/lib/commonjs/createMMKV.js b/node_modules/react-native-mmkv/lib/commonjs/createMMKV.js
|
||||
index b9b9d69..9ec36f9 100644
|
||||
--- a/node_modules/react-native-mmkv/lib/commonjs/createMMKV.js
|
||||
+++ b/node_modules/react-native-mmkv/lib/commonjs/createMMKV.js
|
||||
@@ -38,8 +38,8 @@ const createMMKV = config => {
|
||||
}
|
||||
|
||||
// Check if we are running on-device (JSI)
|
||||
- if (global.nativeCallSyncHook == null || MMKVModule.install == null) {
|
||||
- throw new Error('Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.');
|
||||
+ if (isRemoteDebuggingInChrome() || MMKVModule.install == null) {
|
||||
+ throw new Error(`Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead. isRemoteDebuggingInChrome(): ${isRemoteDebuggingInChrome()}; MMKVModule.install == null: ${MMKVModule.install == null}`);
|
||||
}
|
||||
|
||||
// Call the synchronous blocking install() function
|
||||
@@ -51,5 +51,13 @@ const createMMKV = config => {
|
||||
}
|
||||
return global.mmkvCreateNewInstance(config);
|
||||
};
|
||||
+function isRemoteDebuggingInChrome() {
|
||||
+ // Remote debugging in Chrome is not supported in bridgeless
|
||||
+ if ('RN$Bridgeless' in global && RN$Bridgeless === true) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ return __DEV__ && typeof global.nativeCallSyncHook === 'undefined';
|
||||
+}
|
||||
exports.createMMKV = createMMKV;
|
||||
//# sourceMappingURL=createMMKV.js.map
|
||||
diff --git a/node_modules/react-native-mmkv/lib/module/createMMKV.js b/node_modules/react-native-mmkv/lib/module/createMMKV.js
|
||||
index 644840d..6e704d6 100644
|
||||
--- a/node_modules/react-native-mmkv/lib/module/createMMKV.js
|
||||
+++ b/node_modules/react-native-mmkv/lib/module/createMMKV.js
|
||||
@@ -32,8 +32,8 @@ export const createMMKV = config => {
|
||||
}
|
||||
|
||||
// Check if we are running on-device (JSI)
|
||||
- if (global.nativeCallSyncHook == null || MMKVModule.install == null) {
|
||||
- throw new Error('Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.');
|
||||
+ if (isRemoteDebuggingInChrome() || MMKVModule.install == null) {
|
||||
+ throw new Error(`Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead. isRemoteDebuggingInChrome(): ${isRemoteDebuggingInChrome()}; MMKVModule.install == null: ${MMKVModule.install == null}`);
|
||||
}
|
||||
|
||||
// Call the synchronous blocking install() function
|
||||
@@ -45,4 +45,13 @@ export const createMMKV = config => {
|
||||
}
|
||||
return global.mmkvCreateNewInstance(config);
|
||||
};
|
||||
+
|
||||
+function isRemoteDebuggingInChrome() {
|
||||
+ // Remote debugging in Chrome is not supported in bridgeless
|
||||
+ if ('RN$Bridgeless' in global && RN$Bridgeless === true) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ return __DEV__ && typeof global.nativeCallSyncHook === 'undefined';
|
||||
+}
|
||||
//# sourceMappingURL=createMMKV.js.map
|
||||
diff --git a/node_modules/react-native-mmkv/src/createMMKV.ts b/node_modules/react-native-mmkv/src/createMMKV.ts
|
||||
index 15d29a0..55675fd 100644
|
||||
--- a/node_modules/react-native-mmkv/src/createMMKV.ts
|
||||
+++ b/node_modules/react-native-mmkv/src/createMMKV.ts
|
||||
@@ -46,9 +46,9 @@ export const createMMKV = (config: MMKVConfiguration): NativeMMKV => {
|
||||
}
|
||||
|
||||
// Check if we are running on-device (JSI)
|
||||
- if (global.nativeCallSyncHook == null || MMKVModule.install == null) {
|
||||
+ if (isRemoteDebuggingInChrome() || MMKVModule.install == null) {
|
||||
throw new Error(
|
||||
- 'Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.'
|
||||
+ `Failed to create a new MMKV instance: React Native is not running on-device. MMKV can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead. isRemoteDebuggingInChrome(): ${isRemoteDebuggingInChrome()}; MMKVModule.install == null: ${MMKVModule.install == null}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,3 +68,16 @@ export const createMMKV = (config: MMKVConfiguration): NativeMMKV => {
|
||||
|
||||
return global.mmkvCreateNewInstance(config);
|
||||
};
|
||||
+
|
||||
+declare global {
|
||||
+ const RN$Bridgeless: boolean;
|
||||
+}
|
||||
+
|
||||
+function isRemoteDebuggingInChrome() {
|
||||
+ // Remote debugging in Chrome is not supported in bridgeless
|
||||
+ if ('RN$Bridgeless' in global && RN$Bridgeless === true) {
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ return __DEV__ && typeof global.nativeCallSyncHook === 'undefined';
|
||||
+}
|
||||
@@ -0,0 +1,3 @@
|
||||
# react-native-mmkv+2.12.2.patch.md
|
||||
|
||||
Patch of https://github.com/mrousavy/react-native-mmkv/pull/778
|
||||
@@ -1,44 +0,0 @@
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
index eae3989..432745a 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.cpp
|
||||
@@ -416,6 +416,10 @@ void NativeProxy::progressLayoutAnimation(
|
||||
tag, newPropsJNI, isSharedTransition);
|
||||
}
|
||||
|
||||
+void NativeProxy::endLayoutAnimation(int tag, bool shouldRemove) {
|
||||
+ layoutAnimations_->cthis()->endLayoutAnimation(tag, shouldRemove);
|
||||
+}
|
||||
+
|
||||
PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
|
||||
#ifdef RCT_NEW_ARCH_ENABLED
|
||||
// nothing
|
||||
@@ -455,14 +459,7 @@ PlatformDepMethodsHolder NativeProxy::getPlatformDependentMethods() {
|
||||
auto progressLayoutAnimation =
|
||||
bindThis(&NativeProxy::progressLayoutAnimation);
|
||||
|
||||
- auto endLayoutAnimation = [weakThis = weak_from_this()](
|
||||
- int tag, bool removeView) {
|
||||
- auto strongThis = weakThis.lock();
|
||||
- if (!strongThis) {
|
||||
- return;
|
||||
- }
|
||||
- strongThis->layoutAnimations_->cthis()->endLayoutAnimation(tag, removeView);
|
||||
- };
|
||||
+ auto endLayoutAnimation = bindThis(&NativeProxy::endLayoutAnimation);
|
||||
|
||||
auto maybeFlushUiUpdatesQueueFunction =
|
||||
bindThis(&NativeProxy::maybeFlushUIUpdatesQueue);
|
||||
diff --git a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
index 2ee2cc8..2edb5c9 100644
|
||||
--- a/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
+++ b/node_modules/react-native-reanimated/android/src/main/cpp/reanimated/android/NativeProxy.h
|
||||
@@ -234,6 +234,8 @@ class NativeProxy : public jni::HybridClass<NativeProxy>,
|
||||
const jsi::Object &newProps,
|
||||
bool isSharedTransition);
|
||||
|
||||
+ void endLayoutAnimation(int tag, bool shouldRemove);
|
||||
+
|
||||
/***
|
||||
* Wraps a method of `NativeProxy` in a function object capturing `this`
|
||||
* @tparam TReturn return type of passed method
|
||||
+1
-3
@@ -1,4 +1,3 @@
|
||||
import '#/logger/sentry/setup'
|
||||
import '#/logger/bitdrift/setup'
|
||||
import '#/view/icons'
|
||||
|
||||
@@ -14,7 +13,6 @@ import * as SplashScreen from 'expo-splash-screen'
|
||||
import * as SystemUI from 'expo-system-ui'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
|
||||
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
|
||||
@@ -253,4 +251,4 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Sentry.wrap(App)
|
||||
export default App
|
||||
|
||||
+1
-3
@@ -1,4 +1,3 @@
|
||||
import '#/logger/sentry/setup' // must be near top
|
||||
import '#/view/icons'
|
||||
import './style.css'
|
||||
|
||||
@@ -7,7 +6,6 @@ import {RootSiblingParent} from 'react-native-root-siblings'
|
||||
import {SafeAreaProvider} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
|
||||
import {QueryProvider} from '#/lib/react-query'
|
||||
import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
|
||||
@@ -221,4 +219,4 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
export default Sentry.wrap(App)
|
||||
export default App
|
||||
|
||||
@@ -2,8 +2,6 @@ import {
|
||||
Children,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -26,7 +24,7 @@ import {useA11y} from '#/state/a11y'
|
||||
* screen reader support is enabled. THIS SHOULD BE USED SPARINGLY, only when
|
||||
* no better option is available.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
export function FocusScope({children}: {children: React.ReactNode}) {
|
||||
const {screenReaderEnabled} = useA11y()
|
||||
|
||||
return screenReaderEnabled ? <FocusTrap>{children}</FocusTrap> : children
|
||||
@@ -41,7 +39,7 @@ export function FocusScope({children}: {children: ReactNode}) {
|
||||
* they have reached the start or end of the content and tell them how to
|
||||
* remain within the active content section.
|
||||
*/
|
||||
function FocusTrap({children}: {children: ReactNode}) {
|
||||
function FocusTrap({children}: {children: React.ReactNode}) {
|
||||
const {_} = useLingui()
|
||||
const child = useRef<View>(null)
|
||||
|
||||
@@ -53,7 +51,7 @@ function FocusTrap({children}: {children: ReactNode}) {
|
||||
const decoratedChildren = useMemo(() => {
|
||||
return Children.toArray(children).map((node, i) => {
|
||||
if (i === 0 && isValidElement(node)) {
|
||||
const n = node as ReactElement<any>
|
||||
const n = node as React.ReactElement<any>
|
||||
if (n.props.ref !== undefined) {
|
||||
throw new Error(
|
||||
'FocusScope needs to override the ref on its first child.',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {type ReactNode} from 'react'
|
||||
import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
|
||||
/*
|
||||
@@ -6,7 +5,7 @@ import {FocusScope as RadixFocusScope} from 'radix-ui/internal'
|
||||
* use this in Dialogs and such already. It's here as a convenient counterpart
|
||||
* to the hacky native solution.
|
||||
*/
|
||||
export function FocusScope({children}: {children: ReactNode}) {
|
||||
export function FocusScope({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<RadixFocusScope.FocusScope loop asChild trapped>
|
||||
{children}
|
||||
|
||||
@@ -72,9 +72,9 @@ export function ActionsWrapper({
|
||||
|
||||
return (
|
||||
<View
|
||||
// @ts-expect-error web only
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
// @ts-expect-error web only
|
||||
onFocus={onFocus}
|
||||
onBlur={onMouseLeave}
|
||||
style={[a.flex_1, isFromSelf ? a.flex_row : a.flex_row_reverse]}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {copyAsync} from 'expo-file-system'
|
||||
import {copyAsync} from 'expo-file-system/legacy'
|
||||
import {type BskyAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api'
|
||||
|
||||
import {safeDeleteAsync} from '#/lib/media/manip'
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
makeDirectoryAsync,
|
||||
StorageAccessFramework,
|
||||
writeAsStringAsync,
|
||||
} from 'expo-file-system'
|
||||
} from 'expo-file-system/legacy'
|
||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||
import * as MediaLibrary from 'expo-media-library'
|
||||
import * as Sharing from 'expo-sharing'
|
||||
|
||||
@@ -157,7 +157,7 @@ function createResizedImage(
|
||||
|
||||
export async function saveBytesToDisk(
|
||||
filename: string,
|
||||
bytes: Uint8Array,
|
||||
bytes: Uint8Array<ArrayBuffer>,
|
||||
type: string,
|
||||
) {
|
||||
const blob = new Blob([bytes], {type})
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
documentDirectory,
|
||||
getInfoAsync,
|
||||
readDirectoryAsync,
|
||||
} from 'expo-file-system'
|
||||
} from 'expo-file-system/legacy'
|
||||
import ExpoImageCropTool, {type OpenCropperOptions} from 'expo-image-crop-tool'
|
||||
|
||||
import {compressIfNeeded} from './manip'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createUploadTask, FileSystemUploadType} from 'expo-file-system'
|
||||
import {createUploadTask, FileSystemUploadType} from 'expo-file-system/legacy'
|
||||
import {type AppBskyVideoDefs, type BskyAgent} from '@atproto/api'
|
||||
import {type I18n} from '@lingui/core'
|
||||
import {msg} from '@lingui/macro'
|
||||
|
||||
@@ -1,481 +0,0 @@
|
||||
import {beforeAll, describe, expect, jest, test} from '@jest/globals'
|
||||
import * as Sentry from '@sentry/react-native'
|
||||
import {nanoid} from 'nanoid/non-secure'
|
||||
|
||||
import {Logger} from '#/logger'
|
||||
import {sentryTransport} from '#/logger/transports/sentry'
|
||||
import {LogLevel} from '#/logger/types'
|
||||
|
||||
jest.mock('@sentry/react-native', () => ({
|
||||
addBreadcrumb: jest.fn(),
|
||||
captureException: jest.fn(),
|
||||
captureMessage: jest.fn(),
|
||||
}))
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers()
|
||||
})
|
||||
|
||||
describe('general functionality', () => {
|
||||
test('default params', () => {
|
||||
const logger = new Logger()
|
||||
expect(logger.level).toEqual(LogLevel.Info)
|
||||
})
|
||||
|
||||
test('can override default params', () => {
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
expect(logger.level).toEqual(LogLevel.Debug)
|
||||
})
|
||||
|
||||
test('contextFilter overrides level', () => {
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Info,
|
||||
contextFilter: 'test',
|
||||
})
|
||||
expect(logger.level).toEqual(LogLevel.Debug)
|
||||
})
|
||||
|
||||
test('supports extra metadata', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({})
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
const extra = {foo: true}
|
||||
logger.warn('message', extra)
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
'message',
|
||||
extra,
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('supports nullish/falsy metadata', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({})
|
||||
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
const remove = logger.addTransport(mockTransport)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('a', null)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
'a',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('b', false)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
'b',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('c', 0)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
'c',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
remove()
|
||||
|
||||
logger.addTransport((level, context, message, metadata) => {
|
||||
expect(typeof metadata).toEqual('object')
|
||||
})
|
||||
|
||||
// @ts-expect-error testing the JS case
|
||||
logger.warn('message', null)
|
||||
})
|
||||
|
||||
test('sentryTransport', () => {
|
||||
const message = 'message'
|
||||
const timestamp = Date.now()
|
||||
const sentryTimestamp = timestamp / 1000
|
||||
|
||||
/*
|
||||
sentryTransport(
|
||||
LogLevel.Debug,
|
||||
Logger.Context.Default,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
category: Logger.Context.Default,
|
||||
message,
|
||||
data: {__context__: 'logger'},
|
||||
type: 'default',
|
||||
level: LogLevel.Debug,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
*/
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Info,
|
||||
Logger.Context.Default,
|
||||
message,
|
||||
{type: 'info', prop: true},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
category: Logger.Context.Default,
|
||||
message,
|
||||
data: {prop: true, __context__: 'logger'},
|
||||
type: 'info',
|
||||
level: LogLevel.Info,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Log,
|
||||
Logger.Context.Default,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
category: Logger.Context.Default,
|
||||
message,
|
||||
data: {__context__: 'logger'},
|
||||
type: 'default',
|
||||
level: 'log',
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
jest.runAllTimers()
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||
level: 'log',
|
||||
tags: {category: 'logger'},
|
||||
extra: {__context__: 'logger'},
|
||||
})
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Warn,
|
||||
Logger.Context.Default,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
category: Logger.Context.Default,
|
||||
message,
|
||||
data: {__context__: 'logger'},
|
||||
type: 'default',
|
||||
level: 'warning',
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
jest.runAllTimers()
|
||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||
level: 'warning',
|
||||
tags: {category: 'logger'},
|
||||
extra: {__context__: 'logger'},
|
||||
})
|
||||
|
||||
const e = new Error('error')
|
||||
const tags = {
|
||||
prop: 'prop',
|
||||
}
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Error,
|
||||
Logger.Context.Default,
|
||||
e,
|
||||
{
|
||||
tags,
|
||||
prop: true,
|
||||
},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
expect(Sentry.captureException).toHaveBeenCalledWith(e, {
|
||||
tags: {
|
||||
...tags,
|
||||
category: 'logger',
|
||||
},
|
||||
extra: {
|
||||
prop: true,
|
||||
__context__: 'logger',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('sentryTransport serializes errors', () => {
|
||||
const message = 'message'
|
||||
const timestamp = Date.now()
|
||||
const sentryTimestamp = timestamp / 1000
|
||||
|
||||
sentryTransport(
|
||||
LogLevel.Info,
|
||||
undefined,
|
||||
message,
|
||||
{error: new Error('foo')},
|
||||
timestamp,
|
||||
)
|
||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||
message,
|
||||
data: {error: 'Error: foo'},
|
||||
type: 'default',
|
||||
level: LogLevel.Info,
|
||||
timestamp: sentryTimestamp,
|
||||
})
|
||||
})
|
||||
|
||||
test('add/remove transport', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({})
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
const remove = logger.addTransport(mockTransport)
|
||||
|
||||
logger.warn('warn')
|
||||
|
||||
remove()
|
||||
|
||||
logger.warn('warn')
|
||||
|
||||
// only called once bc it was removed
|
||||
expect(mockTransport).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
'warn',
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
test('create', () => {
|
||||
const mockTransport = jest.fn()
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = Logger.create(Logger.Context.Default)
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.info(message, {})
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Info,
|
||||
Logger.Context.Default,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('debug contexts', () => {
|
||||
test('specific', () => {
|
||||
const mockTransport = jest.fn()
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
// @ts-ignore
|
||||
context: 'specific',
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {})
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
'specific',
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('namespaced', () => {
|
||||
const mockTransport = jest.fn()
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
// @ts-ignore
|
||||
context: 'namespace:foo',
|
||||
contextFilter: 'namespace:*',
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {})
|
||||
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
'namespace:foo',
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('ignores inactive', () => {
|
||||
const mockTransport = jest.fn()
|
||||
const timestamp = Date.now()
|
||||
const message = nanoid()
|
||||
const logger = new Logger({
|
||||
// @ts-ignore
|
||||
context: 'namespace:bar:baz',
|
||||
contextFilter: 'namespace:foo:*',
|
||||
})
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
logger.debug(message, {})
|
||||
|
||||
expect(mockTransport).not.toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
'namespace:bar:baz',
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('supports levels', () => {
|
||||
test('debug', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Debug,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Debug,
|
||||
undefined,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Info,
|
||||
undefined,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
const e = new Error(message)
|
||||
logger.error(e)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Error,
|
||||
undefined,
|
||||
e,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('info', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Info,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Info,
|
||||
undefined,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('warn', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Warn,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Warn,
|
||||
undefined,
|
||||
message,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
|
||||
test('error', () => {
|
||||
const timestamp = Date.now()
|
||||
const logger = new Logger({
|
||||
level: LogLevel.Error,
|
||||
})
|
||||
const message = nanoid()
|
||||
const mockTransport = jest.fn()
|
||||
|
||||
logger.addTransport(mockTransport)
|
||||
|
||||
logger.debug(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.info(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
logger.warn(message)
|
||||
expect(mockTransport).not.toHaveBeenCalled()
|
||||
|
||||
const e = new Error('original message')
|
||||
logger.error(e)
|
||||
expect(mockTransport).toHaveBeenCalledWith(
|
||||
LogLevel.Error,
|
||||
undefined,
|
||||
e,
|
||||
{},
|
||||
timestamp,
|
||||
)
|
||||
})
|
||||
})
|
||||
+1
-4
@@ -5,7 +5,6 @@ import {add} from '#/logger/logDump'
|
||||
import {type MetricEvents} from '#/logger/metrics'
|
||||
import {bitdriftTransport} from '#/logger/transports/bitdrift'
|
||||
import {consoleTransport} from '#/logger/transports/console'
|
||||
import {sentryTransport} from '#/logger/transports/sentry'
|
||||
import {
|
||||
LogContext,
|
||||
LogLevel,
|
||||
@@ -19,9 +18,7 @@ import {ENV} from '#/env'
|
||||
const TRANSPORTS: Transport[] = (function configureTransports() {
|
||||
switch (ENV) {
|
||||
case 'production': {
|
||||
return [sentryTransport, isNative && bitdriftTransport].filter(
|
||||
Boolean,
|
||||
) as Transport[]
|
||||
return [isNative && bitdriftTransport].filter(Boolean) as Transport[]
|
||||
}
|
||||
case 'test': {
|
||||
return []
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * as Sentry from '@sentry/react-native'
|
||||
@@ -1 +0,0 @@
|
||||
export * as Sentry from '@sentry/react-native'
|
||||
@@ -1,32 +0,0 @@
|
||||
import {init} from '@sentry/react-native'
|
||||
|
||||
import * as env from '#/env'
|
||||
|
||||
init({
|
||||
enabled: !env.IS_DEV && !!env.SENTRY_DSN,
|
||||
autoSessionTracking: false,
|
||||
dsn: env.SENTRY_DSN,
|
||||
debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
|
||||
environment: env.ENV,
|
||||
dist: env.BUNDLE_IDENTIFIER,
|
||||
release: env.RELEASE_VERSION,
|
||||
ignoreErrors: [
|
||||
/*
|
||||
* Unknown internals errors
|
||||
*/
|
||||
`t is not defined`,
|
||||
`Can't find variable: t`,
|
||||
/*
|
||||
* Un-useful errors
|
||||
*/
|
||||
`Network request failed`,
|
||||
],
|
||||
/**
|
||||
* Does not affect traces of error events or other logs, just disables
|
||||
* automatically attaching stack traces to events. This helps us group events
|
||||
* and prevents explosions of separate issues.
|
||||
*
|
||||
* @see https://docs.sentry.io/platforms/react-native/configuration/options/#attach-stacktrace
|
||||
*/
|
||||
attachStacktrace: false,
|
||||
})
|
||||
@@ -1,5 +1,4 @@
|
||||
import {isNetworkError} from '#/lib/strings/errors'
|
||||
import {Sentry} from '#/logger/sentry/lib'
|
||||
import {LogLevel, type Transport} from '#/logger/types'
|
||||
import {prepareMetadata} from '#/logger/util'
|
||||
|
||||
@@ -7,8 +6,7 @@ export const sentryTransport: Transport = (
|
||||
level,
|
||||
context,
|
||||
message,
|
||||
{type, tags, ...metadata},
|
||||
timestamp,
|
||||
{tags, ...metadata},
|
||||
) => {
|
||||
// Skip debug messages entirely for now - esb
|
||||
if (level === LogLevel.Debug) return
|
||||
@@ -38,15 +36,6 @@ export const sentryTransport: Transport = (
|
||||
} as const
|
||||
)[level]
|
||||
|
||||
Sentry.addBreadcrumb({
|
||||
category: context,
|
||||
message,
|
||||
data: meta,
|
||||
type: type || 'default',
|
||||
level: severity,
|
||||
timestamp: timestamp / 1000, // Sentry expects seconds
|
||||
})
|
||||
|
||||
// We don't want to send any network errors to sentry
|
||||
if (isNetworkError(message)) {
|
||||
return
|
||||
@@ -68,20 +57,15 @@ export const sentryTransport: Transport = (
|
||||
/**
|
||||
* It's otherwise an Error and should be reported with captureException
|
||||
*/
|
||||
Sentry.captureException(message, {
|
||||
tags: _tags,
|
||||
extra: meta,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] =
|
||||
[]
|
||||
const queuedMessages: [string, Parameters<any>[1]][] = []
|
||||
let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
function queueMessageForSentry(
|
||||
message: string,
|
||||
captureContext: Parameters<typeof Sentry.captureMessage>[1],
|
||||
captureContext: Parameters<any>[1],
|
||||
) {
|
||||
queuedMessages.push([message, captureContext])
|
||||
if (!sentrySendTimeout) {
|
||||
@@ -94,11 +78,4 @@ function queueMessageForSentry(
|
||||
}
|
||||
}
|
||||
|
||||
function sendQueuedMessages() {
|
||||
while (queuedMessages.length > 0) {
|
||||
const record = queuedMessages.shift()
|
||||
if (record) {
|
||||
Sentry.captureMessage(record[0], record[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
function sendQueuedMessages() {}
|
||||
|
||||
@@ -333,9 +333,9 @@ function ChatListItemReady({
|
||||
return (
|
||||
<GestureActionView actions={actions}>
|
||||
<View
|
||||
// @ts-expect-error web only
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
// @ts-expect-error web only
|
||||
onFocus={onFocus}
|
||||
onBlur={onMouseLeave}
|
||||
style={[a.relative, t.atoms.bg]}>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {useMemo} from 'react'
|
||||
import {Platform} from 'react-native'
|
||||
import {setStringAsync} from 'expo-clipboard'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import * as FileSystem from 'expo-file-system/legacy'
|
||||
import {Image} from 'expo-image'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
@@ -420,7 +420,7 @@ function DevOptions() {
|
||||
{
|
||||
style: 'default',
|
||||
text: 'Apply',
|
||||
onPress: channel => {
|
||||
onPress: (channel?: string) => {
|
||||
tryApplyUpdate(channel ?? '')
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
deleteAsync,
|
||||
makeDirectoryAsync,
|
||||
moveAsync,
|
||||
} from 'expo-file-system'
|
||||
} from 'expo-file-system/legacy'
|
||||
import {
|
||||
type Action,
|
||||
type ActionCrop,
|
||||
|
||||
@@ -173,8 +173,10 @@ export const ComposePost = ({
|
||||
imageUris: initImageUris,
|
||||
videoUri: initVideoUri,
|
||||
cancelRef,
|
||||
setIsDirty,
|
||||
}: Props & {
|
||||
cancelRef?: React.RefObject<CancelRef | null>
|
||||
setIsDirty?: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}) => {
|
||||
const {currentAccount} = useSession()
|
||||
const agent = useAgent()
|
||||
@@ -304,24 +306,37 @@ export const ComposePost = ({
|
||||
[insets, isKeyboardVisible],
|
||||
)
|
||||
|
||||
const onPressCancel = useCallback(() => {
|
||||
if (textInput.current?.maybeClosePopup()) {
|
||||
const isDirty = thread.posts.some(
|
||||
post =>
|
||||
post.shortenedGraphemeLength > 0 || post.embed.media || post.embed.link,
|
||||
)
|
||||
|
||||
// very unfortunate, but we need to pass state back up to the parent on iOS
|
||||
//
|
||||
// WARNING - if the Modal on iOS thinks it's not dirty,
|
||||
// `allowSwipeDismissal` will be true, and if we don't then close the composer
|
||||
// when `onPressCancel` is called it will be bad (might even softlock)
|
||||
// so we need to keep the parent state and the behaviour of onPressCancel in
|
||||
// tight sync. do NOT force the modal to stay open without marking it as dirty! -sfn
|
||||
useEffect(() => {
|
||||
if (isIOS) {
|
||||
setIsDirty?.(isDirty)
|
||||
}
|
||||
}, [isDirty, setIsDirty])
|
||||
|
||||
const onPressCancel = useNonReactiveCallback(() => {
|
||||
// web only, so it's fine w.r.t. the Modal
|
||||
const didCloseAutocomplete = textInput.current?.maybeClosePopup()
|
||||
if (isWeb && didCloseAutocomplete) {
|
||||
return
|
||||
} else if (
|
||||
thread.posts.some(
|
||||
post =>
|
||||
post.shortenedGraphemeLength > 0 ||
|
||||
post.embed.media ||
|
||||
post.embed.link,
|
||||
)
|
||||
) {
|
||||
} else if (isDirty) {
|
||||
closeAllDialogs()
|
||||
Keyboard.dismiss()
|
||||
discardPromptControl.open()
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}, [thread, closeAllDialogs, discardPromptControl, onClose])
|
||||
})
|
||||
|
||||
useImperativeHandle(cancelRef, () => ({onPressCancel}))
|
||||
|
||||
|
||||
@@ -1,340 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import * as DropdownMenu from 'zeego/dropdown-menu'
|
||||
import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu'
|
||||
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
import {isIOS} from '#/platform/detection'
|
||||
import {Portal} from '#/components/Portal'
|
||||
|
||||
// Custom Dropdown Menu Components
|
||||
// ==
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuRoot = DropdownMenu.Root
|
||||
// export const DropdownMenuTrigger = DropdownMenu.Trigger
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuContent = DropdownMenu.Content
|
||||
|
||||
type TriggerProps = Omit<
|
||||
React.ComponentProps<(typeof DropdownMenu)['Trigger']>,
|
||||
'children'
|
||||
> &
|
||||
React.PropsWithChildren<{
|
||||
testID?: string
|
||||
accessibilityLabel?: string
|
||||
accessibilityHint?: string
|
||||
}>
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuTrigger = DropdownMenu.create(
|
||||
(props: TriggerProps) => {
|
||||
const theme = useTheme()
|
||||
const defaultCtrlColor = theme.palette.default.postCtrl
|
||||
|
||||
return (
|
||||
// This Pressable doesn't actually do anything other than
|
||||
// provide the "pressed state" visual feedback.
|
||||
<Pressable
|
||||
testID={props.testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
style={({pressed}) => [{opacity: pressed ? 0.8 : 1}]}>
|
||||
<DropdownMenu.Trigger action="press">
|
||||
<View>
|
||||
{props.children ? (
|
||||
props.children
|
||||
) : (
|
||||
<FontAwesomeIcon
|
||||
icon="ellipsis"
|
||||
size={20}
|
||||
color={defaultCtrlColor}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</DropdownMenu.Trigger>
|
||||
</Pressable>
|
||||
)
|
||||
},
|
||||
'Trigger',
|
||||
)
|
||||
|
||||
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuItem = DropdownMenu.create(
|
||||
(props: ItemProps & {testID?: string}) => {
|
||||
const theme = useTheme()
|
||||
const [focused, setFocused] = React.useState(false)
|
||||
const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001'
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
{...props}
|
||||
style={[styles.item, focused && {backgroundColor: backgroundColor}]}
|
||||
onFocus={() => {
|
||||
setFocused(true)
|
||||
props.onFocus && props.onFocus()
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false)
|
||||
props.onBlur && props.onBlur()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
},
|
||||
'Item',
|
||||
)
|
||||
|
||||
type TitleProps = React.ComponentProps<(typeof DropdownMenu)['ItemTitle']>
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuItemTitle = DropdownMenu.create(
|
||||
(props: TitleProps) => {
|
||||
const pal = usePalette('default')
|
||||
return (
|
||||
<DropdownMenu.ItemTitle
|
||||
{...props}
|
||||
style={[props.style, pal.text, styles.itemTitle]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
'ItemTitle',
|
||||
)
|
||||
|
||||
type IconProps = React.ComponentProps<(typeof DropdownMenu)['ItemIcon']>
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuItemIcon = DropdownMenu.create((props: IconProps) => {
|
||||
return <DropdownMenu.ItemIcon {...props} />
|
||||
}, 'ItemIcon')
|
||||
|
||||
type SeparatorProps = React.ComponentProps<(typeof DropdownMenu)['Separator']>
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export const DropdownMenuSeparator = DropdownMenu.create(
|
||||
(props: SeparatorProps) => {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const {borderColor: separatorColor} =
|
||||
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
||||
return (
|
||||
<DropdownMenu.Separator
|
||||
{...props}
|
||||
style={[
|
||||
props.style,
|
||||
styles.separator,
|
||||
{backgroundColor: separatorColor},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
},
|
||||
'Separator',
|
||||
)
|
||||
|
||||
// Types for Dropdown Menu and Items
|
||||
export type DropdownItem = {
|
||||
label: string | 'separator'
|
||||
onPress?: () => void
|
||||
testID?: string
|
||||
icon?: {
|
||||
ios: MenuItemCommonProps['ios']
|
||||
android: string
|
||||
web: IconProp
|
||||
}
|
||||
}
|
||||
type Props = {
|
||||
items: DropdownItem[]
|
||||
testID?: string
|
||||
accessibilityLabel?: string
|
||||
accessibilityHint?: string
|
||||
triggerStyle?: ViewStyle
|
||||
}
|
||||
|
||||
/**
|
||||
* The `NativeDropdown` function uses native iOS and Android dropdown menus.
|
||||
* It also creates a animated custom dropdown for web that uses
|
||||
* Radix UI primitives under the hood
|
||||
* @prop {DropdownItem[]} items - An array of dropdown items
|
||||
* @prop {React.ReactNode} children - A custom dropdown trigger
|
||||
*
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export function NativeDropdown({
|
||||
items,
|
||||
children,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityHint,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
const dropDownBackgroundColor =
|
||||
theme.colorScheme === 'dark' ? pal.btn : pal.viewLight
|
||||
|
||||
return (
|
||||
<>
|
||||
{isIOS && isOpen && (
|
||||
<Portal>
|
||||
<Backdrop />
|
||||
</Portal>
|
||||
)}
|
||||
<DropdownMenuRoot onOpenWillChange={setIsOpen}>
|
||||
<DropdownMenuTrigger
|
||||
action="press"
|
||||
testID={testID}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityHint={accessibilityHint}>
|
||||
{children}
|
||||
</DropdownMenuTrigger>
|
||||
{/* @ts-ignore inheriting props from Radix, which is only for web */}
|
||||
<DropdownMenuContent
|
||||
style={[styles.content, dropDownBackgroundColor]}
|
||||
loop>
|
||||
{items.map((item, index) => {
|
||||
if (item.label === 'separator') {
|
||||
return (
|
||||
<DropdownMenuSeparator
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (index > 1 && items[index - 1].label === 'separator') {
|
||||
return (
|
||||
<DropdownMenu.Group
|
||||
key={getKey(item.label, index, item.testID)}>
|
||||
<DropdownMenuItem
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
onSelect={item.onPress}>
|
||||
<DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle>
|
||||
{item.icon && (
|
||||
<DropdownMenuItemIcon
|
||||
ios={item.icon.ios}
|
||||
// androidIconName={item.icon.android} TODO: Add custom android icon support, because these ones are based on https://developer.android.com/reference/android/R.drawable.html and they are ugly
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={item.icon.web}
|
||||
size={20}
|
||||
style={[pal.text]}
|
||||
/>
|
||||
</DropdownMenuItemIcon>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu.Group>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
onSelect={item.onPress}>
|
||||
<DropdownMenuItemTitle>{item.label}</DropdownMenuItemTitle>
|
||||
{item.icon && (
|
||||
<DropdownMenuItemIcon
|
||||
ios={item.icon.ios}
|
||||
// androidIconName={item.icon.android}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={item.icon.web}
|
||||
size={20}
|
||||
style={[pal.text]}
|
||||
/>
|
||||
</DropdownMenuItemIcon>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuRoot>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Backdrop() {
|
||||
// Not visible but it eats the click outside.
|
||||
// Only necessary for iOS.
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Dialog backdrop"
|
||||
accessibilityHint="Press the backdrop to close the dialog"
|
||||
style={{
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
position: 'absolute',
|
||||
}}
|
||||
onPress={() => {
|
||||
/* noop */
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getKey = (label: string, index: number, id?: string) => {
|
||||
if (id) {
|
||||
return id
|
||||
}
|
||||
return `${label}_${index}`
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: 1,
|
||||
marginVertical: 4,
|
||||
},
|
||||
content: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
paddingVertical: 4,
|
||||
paddingHorizontal: 4,
|
||||
marginTop: 6,
|
||||
...Platform.select({
|
||||
web: {
|
||||
animationDuration: '400ms',
|
||||
animationTimingFunction: 'cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
willChange: 'transform, opacity',
|
||||
animationKeyframes: {
|
||||
'0%': {opacity: 0, transform: [{scale: 0.5}]},
|
||||
'100%': {opacity: 1, transform: [{scale: 1}]},
|
||||
},
|
||||
boxShadow:
|
||||
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
|
||||
transformOrigin: 'var(--radix-dropdown-menu-content-transform-origin)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
columnGap: 20,
|
||||
// @ts-ignore -web
|
||||
cursor: 'pointer',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 8,
|
||||
},
|
||||
itemTitle: {
|
||||
fontSize: 18,
|
||||
},
|
||||
})
|
||||
@@ -1,286 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
type View,
|
||||
type ViewStyle,
|
||||
} from 'react-native'
|
||||
import {type IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {DropdownMenu} from 'radix-ui'
|
||||
import {type MenuItemCommonProps} from 'zeego/lib/typescript/menu'
|
||||
|
||||
import {HITSLOP_10} from '#/lib/constants'
|
||||
import {usePalette} from '#/lib/hooks/usePalette'
|
||||
import {useTheme} from '#/lib/ThemeContext'
|
||||
|
||||
// Custom Dropdown Menu Components
|
||||
// ==
|
||||
export const DropdownMenuRoot = DropdownMenu.Root
|
||||
export const DropdownMenuContent = DropdownMenu.Content
|
||||
|
||||
type ItemProps = React.ComponentProps<(typeof DropdownMenu)['Item']>
|
||||
export const DropdownMenuItem = (props: ItemProps & {testID?: string}) => {
|
||||
const theme = useTheme()
|
||||
const [focused, setFocused] = React.useState(false)
|
||||
const backgroundColor = theme.colorScheme === 'dark' ? '#fff1' : '#0001'
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
className="nativeDropdown-item"
|
||||
{...props}
|
||||
style={StyleSheet.flatten([
|
||||
styles.item,
|
||||
focused && {backgroundColor: backgroundColor},
|
||||
])}
|
||||
onFocus={() => {
|
||||
setFocused(true)
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Types for Dropdown Menu and Items
|
||||
export type DropdownItem = {
|
||||
label: string | 'separator'
|
||||
onPress?: () => void
|
||||
testID?: string
|
||||
icon?: {
|
||||
ios: MenuItemCommonProps['ios']
|
||||
android: string
|
||||
web: IconProp
|
||||
}
|
||||
}
|
||||
type Props = {
|
||||
items: DropdownItem[]
|
||||
testID?: string
|
||||
accessibilityLabel?: string
|
||||
accessibilityHint?: string
|
||||
triggerStyle?: ViewStyle
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use Menu from `#/components/Menu.tsx` instead
|
||||
*/
|
||||
export function NativeDropdown({
|
||||
items,
|
||||
children,
|
||||
testID,
|
||||
accessibilityLabel,
|
||||
accessibilityHint,
|
||||
triggerStyle,
|
||||
}: React.PropsWithChildren<Props>) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const buttonRef = React.useRef<HTMLButtonElement>(null)
|
||||
const menuRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!open) {
|
||||
return
|
||||
}
|
||||
|
||||
function clickHandler(e: MouseEvent) {
|
||||
const t = e.target
|
||||
|
||||
if (!open) return
|
||||
if (!t) return
|
||||
if (!buttonRef.current || !menuRef.current) return
|
||||
|
||||
if (
|
||||
t !== buttonRef.current &&
|
||||
!buttonRef.current.contains(t as Node) &&
|
||||
t !== menuRef.current &&
|
||||
!menuRef.current.contains(t as Node)
|
||||
) {
|
||||
// prevent clicking through to links beneath dropdown
|
||||
// only applies to mobile web
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// close menu
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
function keydownHandler(e: KeyboardEvent) {
|
||||
if (e.key === 'Escape' && open) {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', clickHandler, true)
|
||||
window.addEventListener('keydown', keydownHandler, true)
|
||||
return () => {
|
||||
document.removeEventListener('click', clickHandler, true)
|
||||
window.removeEventListener('keydown', keydownHandler, true)
|
||||
}
|
||||
}, [open, setOpen])
|
||||
|
||||
return (
|
||||
<DropdownMenuRoot open={open} onOpenChange={o => setOpen(o)}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<Pressable
|
||||
ref={buttonRef as unknown as React.Ref<View>}
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityHint={accessibilityHint}
|
||||
onPointerDown={e => {
|
||||
// Prevent false positive that interpret mobile scroll as a tap.
|
||||
// This requires the custom onPress handler below to compensate.
|
||||
// https://github.com/radix-ui/primitives/issues/1912
|
||||
e.preventDefault()
|
||||
}}
|
||||
onPress={() => {
|
||||
if (window.event instanceof KeyboardEvent) {
|
||||
// The onPointerDown hack above is not relevant to this press, so don't do anything.
|
||||
return
|
||||
}
|
||||
// Compensate for the disabled onPointerDown above by triggering it manually.
|
||||
setOpen(o => !o)
|
||||
}}
|
||||
hitSlop={HITSLOP_10}
|
||||
style={triggerStyle}>
|
||||
{children}
|
||||
</Pressable>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownContent items={items} menuRef={menuRef} />
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
function DropdownContent({
|
||||
items,
|
||||
menuRef,
|
||||
}: {
|
||||
items: DropdownItem[]
|
||||
menuRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const pal = usePalette('default')
|
||||
const theme = useTheme()
|
||||
const dropDownBackgroundColor =
|
||||
theme.colorScheme === 'dark' ? pal.btn : pal.view
|
||||
const {borderColor: separatorColor} =
|
||||
theme.colorScheme === 'dark' ? pal.borderDark : pal.border
|
||||
|
||||
return (
|
||||
<DropdownMenu.Content
|
||||
ref={menuRef}
|
||||
style={
|
||||
StyleSheet.flatten([
|
||||
styles.content,
|
||||
dropDownBackgroundColor,
|
||||
]) as React.CSSProperties
|
||||
}
|
||||
loop>
|
||||
{items.map((item, index) => {
|
||||
if (item.label === 'separator') {
|
||||
return (
|
||||
<DropdownMenu.Separator
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
style={
|
||||
StyleSheet.flatten([
|
||||
styles.separator,
|
||||
{backgroundColor: separatorColor},
|
||||
]) as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
if (index > 1 && items[index - 1].label === 'separator') {
|
||||
return (
|
||||
<DropdownMenu.Group key={getKey(item.label, index, item.testID)}>
|
||||
<DropdownMenuItem
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
onSelect={item.onPress}>
|
||||
<Text selectable={false} style={[pal.text, styles.itemTitle]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={item.icon.web}
|
||||
size={20}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu.Group>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={getKey(item.label, index, item.testID)}
|
||||
onSelect={item.onPress}>
|
||||
<Text selectable={false} style={[pal.text, styles.itemTitle]}>
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.icon && (
|
||||
<FontAwesomeIcon
|
||||
icon={item.icon.web}
|
||||
size={20}
|
||||
color={pal.colors.textLight}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
})}
|
||||
</DropdownMenu.Content>
|
||||
)
|
||||
}
|
||||
|
||||
const getKey = (label: string, index: number, id?: string) => {
|
||||
if (id) {
|
||||
return id
|
||||
}
|
||||
return `${label}_${index}`
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
separator: {
|
||||
height: 1,
|
||||
marginTop: 4,
|
||||
marginBottom: 4,
|
||||
},
|
||||
content: {
|
||||
backgroundColor: '#f0f0f0',
|
||||
borderRadius: 8,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
paddingLeft: 4,
|
||||
paddingRight: 4,
|
||||
marginTop: 6,
|
||||
|
||||
// @ts-ignore web only -prf
|
||||
boxShadow: 'rgba(0, 0, 0, 0.3) 0px 5px 20px',
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
columnGap: 20,
|
||||
cursor: 'pointer',
|
||||
paddingTop: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
borderRadius: 8,
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Liberation Sans", Helvetica, Arial, sans-serif',
|
||||
// @ts-expect-error web only
|
||||
outline: 0,
|
||||
border: 0,
|
||||
},
|
||||
itemTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
paddingRight: 10,
|
||||
},
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react'
|
||||
import {useEffect, useRef, useState} from 'react'
|
||||
import {Modal, View} from 'react-native'
|
||||
|
||||
import {useDialogStateControlContext} from '#/state/dialogs'
|
||||
@@ -11,11 +11,17 @@ export function Composer({}: {winHeight: number}) {
|
||||
const t = useTheme()
|
||||
const state = useComposerState()
|
||||
const ref = useComposerCancelRef()
|
||||
const [isDirty, setIsDirty] = useState(
|
||||
!!state?.text ||
|
||||
!!state?.imageUris ||
|
||||
!!state?.videoUri ||
|
||||
!!state?.mention,
|
||||
)
|
||||
|
||||
const open = !!state
|
||||
const prevOpen = React.useRef(open)
|
||||
const prevOpen = useRef(open)
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (open && !prevOpen.current) {
|
||||
setFullyExpandedCount(c => c + 1)
|
||||
} else if (!open && prevOpen.current) {
|
||||
@@ -31,7 +37,8 @@ export function Composer({}: {winHeight: number}) {
|
||||
visible={open}
|
||||
presentationStyle="pageSheet"
|
||||
animationType="slide"
|
||||
onRequestClose={() => ref.current?.onPressCancel()}>
|
||||
onRequestClose={() => ref.current?.onPressCancel()}
|
||||
allowSwipeDismissal={!isDirty}>
|
||||
<View style={[t.atoms.bg, a.flex_1]}>
|
||||
<ComposePost
|
||||
cancelRef={ref}
|
||||
@@ -43,6 +50,7 @@ export function Composer({}: {winHeight: number}) {
|
||||
text={state?.text}
|
||||
imageUris={state?.imageUris}
|
||||
videoUri={state?.videoUri}
|
||||
setIsDirty={setIsDirty}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
Reference in New Issue
Block a user