Merge remote-tracking branch 'origin/main' into next/embeds
* origin/main: new arch (#8295) Nightly source-language update Loosen post source constraints (#8478) hide keyboard when backgrounding (#8450)
This commit is contained in:
@@ -1,10 +1,9 @@
|
|||||||
import {deleteAsync} from 'expo-file-system'
|
import {createDownloadResumable, deleteAsync} from 'expo-file-system'
|
||||||
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
||||||
import RNFetchBlob from 'rn-fetch-blob'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
downloadAndResize,
|
downloadAndResize,
|
||||||
DownloadAndResizeOpts,
|
type DownloadAndResizeOpts,
|
||||||
getResizedDimensions,
|
getResizedDimensions,
|
||||||
} from '../../src/lib/media/manip'
|
} from '../../src/lib/media/manip'
|
||||||
|
|
||||||
@@ -32,11 +31,12 @@ describe('downloadAndResize', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return resized image for valid URI and options', async () => {
|
it('should return resized image for valid URI and options', async () => {
|
||||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
const mockedFetch = createDownloadResumable as jest.Mock
|
||||||
mockedFetch.mockResolvedValueOnce({
|
mockedFetch.mockReturnValue({
|
||||||
path: jest.fn().mockReturnValue('file://downloaded-image.jpg'),
|
cancelAsync: jest.fn(),
|
||||||
info: jest.fn().mockReturnValue({status: 200}),
|
downloadAsync: jest
|
||||||
flush: jest.fn(),
|
.fn()
|
||||||
|
.mockResolvedValue({uri: 'file://resized-image.jpg'}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const opts: DownloadAndResizeOpts = {
|
const opts: DownloadAndResizeOpts = {
|
||||||
@@ -50,13 +50,12 @@ describe('downloadAndResize', () => {
|
|||||||
|
|
||||||
const result = await downloadAndResize(opts)
|
const result = await downloadAndResize(opts)
|
||||||
expect(result).toEqual(mockResizedImage)
|
expect(result).toEqual(mockResizedImage)
|
||||||
expect(RNFetchBlob.config).toHaveBeenCalledWith({
|
expect(createDownloadResumable).toHaveBeenCalledWith(
|
||||||
fileCache: true,
|
opts.uri,
|
||||||
appendExt: 'jpeg',
|
expect.anything(),
|
||||||
})
|
{
|
||||||
expect(RNFetchBlob.fetch).toHaveBeenCalledWith(
|
cache: true,
|
||||||
'GET',
|
},
|
||||||
'https://example.com/image.jpg',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// First time it gets called is to get dimensions
|
// First time it gets called is to get dimensions
|
||||||
@@ -86,28 +85,6 @@ describe('downloadAndResize', () => {
|
|||||||
expect(result).toBeUndefined()
|
expect(result).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should return undefined for non-200 response', async () => {
|
|
||||||
const mockedFetch = RNFetchBlob.fetch as jest.Mock
|
|
||||||
mockedFetch.mockResolvedValueOnce({
|
|
||||||
path: jest.fn().mockReturnValue('file://downloaded-image'),
|
|
||||||
info: jest.fn().mockReturnValue({status: 400}),
|
|
||||||
flush: jest.fn(),
|
|
||||||
})
|
|
||||||
|
|
||||||
const opts: DownloadAndResizeOpts = {
|
|
||||||
uri: 'https://example.com/image',
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
maxSize: 500000,
|
|
||||||
mode: 'cover',
|
|
||||||
timeout: 10000,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await downloadAndResize(opts)
|
|
||||||
expect(errorSpy).not.toHaveBeenCalled()
|
|
||||||
expect(result).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
it('should not downsize whenever dimensions are below the max dimensions', () => {
|
||||||
const initialDimensionsOne = {
|
const initialDimensionsOne = {
|
||||||
width: 1200,
|
width: 1200,
|
||||||
|
|||||||
+1
-1
@@ -219,7 +219,7 @@ module.exports = function (_config) {
|
|||||||
compileSdkVersion: 35,
|
compileSdkVersion: 35,
|
||||||
targetSdkVersion: 35,
|
targetSdkVersion: 35,
|
||||||
buildToolsVersion: '35.0.0',
|
buildToolsVersion: '35.0.0',
|
||||||
newArchEnabled: false,
|
newArchEnabled: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
+2
-7
@@ -33,15 +33,10 @@ jest.mock('react-native-safe-area-context', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.mock('rn-fetch-blob', () => ({
|
|
||||||
config: jest.fn().mockReturnThis(),
|
|
||||||
cancel: jest.fn(),
|
|
||||||
fetch: jest.fn(),
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('expo-file-system', () => ({
|
jest.mock('expo-file-system', () => ({
|
||||||
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
getInfoAsync: jest.fn().mockResolvedValue({exists: true, size: 100}),
|
||||||
deleteAsync: jest.fn(),
|
deleteAsync: jest.fn(),
|
||||||
|
createDownloadResumable: jest.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
jest.mock('expo-image-manipulator', () => ({
|
jest.mock('expo-image-manipulator', () => ({
|
||||||
@@ -101,7 +96,7 @@ jest.mock('expo-modules-core', () => ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
requireNativeViewManager: jest.fn().mockImplementation(moduleName => {
|
requireNativeViewManager: jest.fn().mockImplementation(_ => {
|
||||||
return () => null
|
return () => null
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|||||||
+40
-38
@@ -74,7 +74,7 @@
|
|||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@expo/html-elements": "^0.12.4",
|
"@expo/html-elements": "^0.12.5",
|
||||||
"@expo/webpack-config": "^19.0.1",
|
"@expo/webpack-config": "^19.0.1",
|
||||||
"@floating-ui/dom": "^1.6.3",
|
"@floating-ui/dom": "^1.6.3",
|
||||||
"@floating-ui/react-dom": "^2.0.8",
|
"@floating-ui/react-dom": "^2.0.8",
|
||||||
@@ -85,12 +85,12 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||||
"@haileyok/bluesky-video": "0.2.6",
|
"@haileyok/bluesky-video": "0.3.1",
|
||||||
"@ipld/dag-cbor": "^9.2.0",
|
"@ipld/dag-cbor": "^9.2.0",
|
||||||
"@lingui/react": "^4.14.1",
|
"@lingui/react": "^4.14.1",
|
||||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
"@mattermost/react-native-paste-input": "mattermost/react-native-paste-input",
|
||||||
"@miblanchard/react-native-slider": "^2.3.1",
|
"@miblanchard/react-native-slider": "^2.6.0",
|
||||||
"@mozzius/expo-dynamic-app-icon": "^1.5.0",
|
"@mozzius/expo-dynamic-app-icon": "1.5.0",
|
||||||
"@react-native-async-storage/async-storage": "2.1.2",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
"@react-native-menu/menu": "^1.2.3",
|
"@react-native-menu/menu": "^1.2.3",
|
||||||
"@react-native-picker/picker": "2.11.0",
|
"@react-native-picker/picker": "2.11.0",
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"@react-navigation/drawer": "^7.3.12",
|
"@react-navigation/drawer": "^7.3.12",
|
||||||
"@react-navigation/native": "^7.1.9",
|
"@react-navigation/native": "^7.1.9",
|
||||||
"@react-navigation/native-stack": "^7.3.13",
|
"@react-navigation/native-stack": "^7.3.13",
|
||||||
"@sentry/react-native": "~6.10.0",
|
"@sentry/react-native": "~6.14.0",
|
||||||
"@tanstack/query-async-storage-persister": "^5.25.0",
|
"@tanstack/query-async-storage-persister": "^5.25.0",
|
||||||
"@tanstack/react-query": "^5.8.1",
|
"@tanstack/react-query": "^5.8.1",
|
||||||
"@tanstack/react-query-persist-client": "^5.25.0",
|
"@tanstack/react-query-persist-client": "^5.25.0",
|
||||||
@@ -130,33 +130,33 @@
|
|||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"emoji-regex": "^10.4.0",
|
"emoji-regex": "^10.4.0",
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.1",
|
||||||
"expo": "^53.0.5",
|
"expo": "53.0.11",
|
||||||
"expo-application": "~6.1.4",
|
"expo-application": "~6.1.4",
|
||||||
"expo-blur": "~14.1.4",
|
"expo-blur": "~14.1.5",
|
||||||
"expo-build-properties": "~0.14.6",
|
"expo-build-properties": "~0.14.6",
|
||||||
"expo-camera": "~16.1.6",
|
"expo-camera": "~16.1.8",
|
||||||
"expo-clipboard": "~7.1.4",
|
"expo-clipboard": "~7.1.4",
|
||||||
"expo-dev-client": "~5.1.7",
|
"expo-dev-client": "~5.2.0",
|
||||||
"expo-device": "~7.1.4",
|
"expo-device": "~7.1.4",
|
||||||
"expo-file-system": "~18.1.8",
|
"expo-file-system": "~18.1.10",
|
||||||
"expo-font": "~13.3.0",
|
"expo-font": "~13.3.1",
|
||||||
"expo-haptics": "~14.1.4",
|
"expo-haptics": "~14.1.4",
|
||||||
"expo-image": "~2.1.6",
|
"expo-image": "~2.2.1",
|
||||||
"expo-image-crop-tool": "^0.1.8",
|
"expo-image-crop-tool": "^0.1.8",
|
||||||
"expo-image-manipulator": "~13.1.5",
|
"expo-image-manipulator": "~13.1.7",
|
||||||
"expo-image-picker": "~16.1.4",
|
"expo-image-picker": "~16.1.4",
|
||||||
"expo-linear-gradient": "~14.1.4",
|
"expo-linear-gradient": "~14.1.5",
|
||||||
"expo-linking": "~7.1.4",
|
"expo-linking": "~7.1.5",
|
||||||
"expo-localization": "~16.1.5",
|
"expo-localization": "~16.1.5",
|
||||||
"expo-media-library": "~17.1.6",
|
"expo-media-library": "~17.1.7",
|
||||||
"expo-notifications": "~0.31.1",
|
"expo-notifications": "~0.31.3",
|
||||||
"expo-screen-orientation": "~8.1.5",
|
"expo-screen-orientation": "~8.1.7",
|
||||||
"expo-sharing": "~13.1.5",
|
"expo-sharing": "~13.1.5",
|
||||||
"expo-splash-screen": "~0.30.8",
|
"expo-splash-screen": "~0.30.9",
|
||||||
"expo-system-ui": "~5.0.7",
|
"expo-system-ui": "~5.0.8",
|
||||||
"expo-task-manager": "~13.1.5",
|
"expo-task-manager": "~13.1.5",
|
||||||
"expo-updates": "~0.28.12",
|
"expo-updates": "~0.28.14",
|
||||||
"expo-video": "~2.1.8",
|
"expo-video": "~2.2.1",
|
||||||
"expo-web-browser": "~14.1.6",
|
"expo-web-browser": "~14.1.6",
|
||||||
"fast-text-encoding": "^1.0.6",
|
"fast-text-encoding": "^1.0.6",
|
||||||
"history": "^5.3.0",
|
"history": "^5.3.0",
|
||||||
@@ -182,35 +182,34 @@
|
|||||||
"react-image-crop": "^11.0.7",
|
"react-image-crop": "^11.0.7",
|
||||||
"react-is": "19",
|
"react-is": "19",
|
||||||
"react-keyed-flatten-children": "^5.0.0",
|
"react-keyed-flatten-children": "^5.0.0",
|
||||||
"react-native": "0.79.2",
|
"react-native": "^0.79.3",
|
||||||
"react-native-compressor": "1.11.0",
|
"react-native-compressor": "^1.11.0",
|
||||||
"react-native-date-picker": "^5.0.12",
|
"react-native-date-picker": "^5.0.12",
|
||||||
"react-native-drawer-layout": "^4.1.6",
|
"react-native-drawer-layout": "^4.1.8",
|
||||||
"react-native-edge-to-edge": "^1.6.0",
|
"react-native-edge-to-edge": "^1.6.0",
|
||||||
"react-native-gesture-handler": "2.25.0",
|
"react-native-gesture-handler": "2.25.0",
|
||||||
"react-native-get-random-values": "~1.11.0",
|
"react-native-get-random-values": "~1.11.0",
|
||||||
"react-native-ios-context-menu": "^1.15.3",
|
"react-native-ios-context-menu": "^1.15.3",
|
||||||
"react-native-keyboard-controller": "^1.17.1",
|
"react-native-keyboard-controller": "^1.17.1",
|
||||||
"react-native-mmkv": "^2.12.2",
|
"react-native-mmkv": "^2.12.2",
|
||||||
"react-native-pager-view": "6.7.1",
|
"react-native-pager-view": "^6.7.1",
|
||||||
"react-native-progress": "bluesky-social/react-native-progress",
|
"react-native-progress": "bluesky-social/react-native-progress",
|
||||||
"react-native-qrcode-styled": "^0.3.3",
|
"react-native-qrcode-styled": "^0.3.3",
|
||||||
"react-native-reanimated": "~3.17.5",
|
"react-native-reanimated": "~3.17.5",
|
||||||
"react-native-root-siblings": "^4.1.1",
|
"react-native-root-siblings": "^5.0.1",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "^4.11.1",
|
"react-native-screens": "^4.11.1",
|
||||||
"react-native-svg": "15.11.2",
|
"react-native-svg": "15.12.0",
|
||||||
"react-native-uitextview": "^1.4.0",
|
"react-native-uitextview": "^1.4.0",
|
||||||
"react-native-url-polyfill": "^1.3.0",
|
"react-native-url-polyfill": "^1.3.0",
|
||||||
"react-native-uuid": "^2.0.3",
|
"react-native-uuid": "^2.0.3",
|
||||||
"react-native-view-shot": "^4.0.3",
|
"react-native-view-shot": "^4.0.3",
|
||||||
"react-native-web": "~0.20.0",
|
"react-native-web": "~0.20.0",
|
||||||
"react-native-web-webview": "^1.0.2",
|
"react-native-web-webview": "^1.0.2",
|
||||||
"react-native-webview": "13.13.5",
|
"react-native-webview": "^13.13.5",
|
||||||
"react-remove-scroll-bar": "^2.3.8",
|
"react-remove-scroll-bar": "^2.3.8",
|
||||||
"react-responsive": "^9.0.2",
|
"react-responsive": "^9.0.2",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
"rn-fetch-blob": "^0.12.0",
|
|
||||||
"statsig-react-native-expo": "^4.6.1",
|
"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",
|
||||||
@@ -227,8 +226,9 @@
|
|||||||
"@lingui/cli": "^4.14.1",
|
"@lingui/cli": "^4.14.1",
|
||||||
"@lingui/macro": "^4.14.1",
|
"@lingui/macro": "^4.14.1",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
||||||
"@react-native/eslint-config": "^0.79.2",
|
"@react-native/babel-preset": "0.79.3",
|
||||||
"@react-native/typescript-config": "^0.79.2",
|
"@react-native/eslint-config": "^0.79.3",
|
||||||
|
"@react-native/typescript-config": "^0.79.3",
|
||||||
"@sentry/webpack-plugin": "^3.2.2",
|
"@sentry/webpack-plugin": "^3.2.2",
|
||||||
"@testing-library/jest-native": "^5.4.3",
|
"@testing-library/jest-native": "^5.4.3",
|
||||||
"@testing-library/react-native": "^13.2.0",
|
"@testing-library/react-native": "^13.2.0",
|
||||||
@@ -247,6 +247,7 @@
|
|||||||
"babel-plugin-module-resolver": "^5.0.2",
|
"babel-plugin-module-resolver": "^5.0.2",
|
||||||
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
"babel-plugin-react-compiler": "^19.1.0-rc.1",
|
||||||
"babel-preset-expo": "~13.1.11",
|
"babel-preset-expo": "~13.1.11",
|
||||||
|
"browserslist": "^4.25.0",
|
||||||
"eslint": "^8.19.0",
|
"eslint": "^8.19.0",
|
||||||
"eslint-plugin-bsky-internal": "link:./eslint",
|
"eslint-plugin-bsky-internal": "link:./eslint",
|
||||||
"eslint-plugin-ft-flow": "^2.0.3",
|
"eslint-plugin-ft-flow": "^2.0.3",
|
||||||
@@ -260,7 +261,7 @@
|
|||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expo": "~53.0.3",
|
"jest-expo": "~53.0.7",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
"lint-staged": "^13.2.3",
|
"lint-staged": "^13.2.3",
|
||||||
"lockfile-lint": "^4.14.0",
|
"lockfile-lint": "^4.14.0",
|
||||||
@@ -275,11 +276,11 @@
|
|||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@expo/image-utils": "0.6.3",
|
"@expo/image-utils": "0.6.3",
|
||||||
"@react-native/babel-preset": "0.79.2",
|
"@react-native/babel-preset": "0.79.3",
|
||||||
"@react-native/normalize-colors": "0.79.2",
|
"@react-native/normalize-colors": "0.79.3",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"**/expo-constants": "17.0.3",
|
"**/expo-constants": "17.0.3",
|
||||||
"**/expo-device": "7.0.1",
|
"**/expo-device": "7.1.4",
|
||||||
"**/zod": "3.23.8",
|
"**/zod": "3.23.8",
|
||||||
"**/multiformats": "9.9.0"
|
"**/multiformats": "9.9.0"
|
||||||
},
|
},
|
||||||
@@ -359,7 +360,8 @@
|
|||||||
],
|
],
|
||||||
"allowedUrls": [
|
"allowedUrls": [
|
||||||
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
"https://codeload.github.com/bluesky-social/react-native-bottom-sheet/tar.gz/28a87d1bb55e10fc355fa1455545a30734995908",
|
||||||
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4"
|
"https://codeload.github.com/bluesky-social/react-native-progress/tar.gz/5a372f4f2ce5feb26f4f47b6a4d187ab9b923ab4",
|
||||||
|
"https://codeload.github.com/mattermost/react-native-paste-input/tar.gz/f260447edc645a817ab1ba7b46d8341d84dba8e9"
|
||||||
],
|
],
|
||||||
"emptyHostname": false,
|
"emptyHostname": false,
|
||||||
"validatePackageNames": true,
|
"validatePackageNames": true,
|
||||||
|
|||||||
+109
-2
@@ -114,7 +114,7 @@ index e916023..5049c33 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
#pragma mark - UIScrollViewDelegate
|
#pragma mark - UIScrollViewDelegate
|
||||||
@@ -62,7 +92,6 @@
|
@@ -62,7 +92,6 @@ - (void)setSmartPunctuation:(NSString *)smartPunctuation {
|
||||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||||
{
|
{
|
||||||
RCTDirectEventBlock onScroll = self.onScroll;
|
RCTDirectEventBlock onScroll = self.onScroll;
|
||||||
@@ -122,7 +122,7 @@ index e916023..5049c33 100644
|
|||||||
if (onScroll) {
|
if (onScroll) {
|
||||||
CGPoint contentOffset = scrollView.contentOffset;
|
CGPoint contentOffset = scrollView.contentOffset;
|
||||||
CGSize contentSize = scrollView.contentSize;
|
CGSize contentSize = scrollView.contentSize;
|
||||||
@@ -71,22 +100,22 @@
|
@@ -71,22 +100,22 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||||
|
|
||||||
onScroll(@{
|
onScroll(@{
|
||||||
@"contentOffset": @{
|
@"contentOffset": @{
|
||||||
@@ -155,3 +155,110 @@ index e916023..5049c33 100644
|
|||||||
},
|
},
|
||||||
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
@"zoomScale": @(scrollView.zoomScale ?: 1),
|
||||||
});
|
});
|
||||||
|
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||||
|
index dd50053..2ed7017 100644
|
||||||
|
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||||
|
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInput.mm
|
||||||
|
@@ -122,8 +122,8 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
|
||||||
|
const auto &newTextInputProps = static_cast<const PasteTextInputProps &>(*props);
|
||||||
|
|
||||||
|
// Traits:
|
||||||
|
- if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
|
||||||
|
- [self _setMultiline:newTextInputProps.traits.multiline];
|
||||||
|
+ if (newTextInputProps.multiline != oldTextInputProps.multiline) {
|
||||||
|
+ [self _setMultiline:newTextInputProps.multiline];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
|
||||||
|
@@ -421,7 +421,7 @@ - (void)textInputDidChangeSelection
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||||
|
- if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||||
|
+ if (props.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
|
||||||
|
[self textInputDidChange];
|
||||||
|
_ignoreNextTextInputCall = YES;
|
||||||
|
}
|
||||||
|
@@ -708,11 +708,11 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
|
||||||
|
- (SubmitBehavior)getSubmitBehavior
|
||||||
|
{
|
||||||
|
const auto &props = static_cast<const PasteTextInputProps &>(*_props);
|
||||||
|
- const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
|
||||||
|
+ const SubmitBehavior submitBehaviorDefaultable = props.submitBehavior;
|
||||||
|
|
||||||
|
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
|
||||||
|
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
|
||||||
|
- return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||||
|
+ return props.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
return submitBehaviorDefaultable;
|
||||||
|
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||||
|
index 29e094f..7ef519a 100644
|
||||||
|
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||||
|
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.cpp
|
||||||
|
@@ -22,8 +22,7 @@ PasteTextInputProps::PasteTextInputProps(
|
||||||
|
const PropsParserContext &context,
|
||||||
|
const PasteTextInputProps &sourceProps,
|
||||||
|
const RawProps& rawProps)
|
||||||
|
- : ViewProps(context, sourceProps, rawProps),
|
||||||
|
- BaseTextProps(context, sourceProps, rawProps),
|
||||||
|
+ : BaseTextInputProps(context, sourceProps, rawProps),
|
||||||
|
traits(convertRawProp(context, rawProps, sourceProps.traits, {})),
|
||||||
|
smartPunctuation(convertRawProp(context, rawProps, "smartPunctuation", sourceProps.smartPunctuation, {})),
|
||||||
|
disableCopyPaste(convertRawProp(context, rawProps, "disableCopyPaste", sourceProps.disableCopyPaste, {false})),
|
||||||
|
@@ -133,7 +132,7 @@ TextAttributes PasteTextInputProps::getEffectiveTextAttributes(Float fontSizeMul
|
||||||
|
ParagraphAttributes PasteTextInputProps::getEffectiveParagraphAttributes() const {
|
||||||
|
auto result = paragraphAttributes;
|
||||||
|
|
||||||
|
- if (!traits.multiline) {
|
||||||
|
+ if (!multiline) {
|
||||||
|
result.maximumNumberOfLines = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||||
|
index 723d00c..31cfe66 100644
|
||||||
|
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||||
|
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/Props.h
|
||||||
|
@@ -15,6 +15,7 @@
|
||||||
|
#include <react/renderer/components/iostextinput/conversions.h>
|
||||||
|
#include <react/renderer/components/iostextinput/primitives.h>
|
||||||
|
#include <react/renderer/components/text/BaseTextProps.h>
|
||||||
|
+#include <react/renderer/components/textinput/BaseTextInputProps.h>
|
||||||
|
#include <react/renderer/components/view/ViewProps.h>
|
||||||
|
#include <react/renderer/core/Props.h>
|
||||||
|
#include <react/renderer/core/PropsParserContext.h>
|
||||||
|
@@ -25,7 +26,7 @@
|
||||||
|
|
||||||
|
namespace facebook::react {
|
||||||
|
|
||||||
|
-class PasteTextInputProps final : public ViewProps, public BaseTextProps {
|
||||||
|
+class PasteTextInputProps final : public BaseTextInputProps {
|
||||||
|
public:
|
||||||
|
PasteTextInputProps() = default;
|
||||||
|
PasteTextInputProps(const PropsParserContext& context, const PasteTextInputProps& sourceProps, const RawProps& rawProps);
|
||||||
|
diff --git a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||||
|
index 31e07e3..7f0ebfb 100644
|
||||||
|
--- a/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||||
|
+++ b/node_modules/@mattermost/react-native-paste-input/ios/PasteTextInputSpecs/ShadowNodes.cpp
|
||||||
|
@@ -91,20 +91,11 @@ void PasteTextInputShadowNode::updateStateIfNeeded(
|
||||||
|
const auto& state = getStateData();
|
||||||
|
|
||||||
|
react_native_assert(textLayoutManager_);
|
||||||
|
- react_native_assert(
|
||||||
|
- (!state.layoutManager || state.layoutManager == textLayoutManager_) &&
|
||||||
|
- "`StateData` refers to a different `TextLayoutManager`");
|
||||||
|
-
|
||||||
|
- if (state.reactTreeAttributedString == reactTreeAttributedString &&
|
||||||
|
- state.layoutManager == textLayoutManager_) {
|
||||||
|
- return;
|
||||||
|
- }
|
||||||
|
|
||||||
|
auto newState = TextInputState{};
|
||||||
|
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
|
||||||
|
newState.paragraphAttributes = getConcreteProps().paragraphAttributes;
|
||||||
|
newState.reactTreeAttributedString = reactTreeAttributedString;
|
||||||
|
- newState.layoutManager = textLayoutManager_;
|
||||||
|
newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount;
|
||||||
|
setStateData(std::move(newState));
|
||||||
|
}
|
||||||
@@ -1,3 +1,32 @@
|
|||||||
|
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||||
|
index 914a249..0deac55 100644
|
||||||
|
--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||||
|
+++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTPullToRefreshViewComponentView.h
|
||||||
|
@@ -19,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
*/
|
||||||
|
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
|
||||||
|
|
||||||
|
+- (void)beginRefreshingProgrammatically;
|
||||||
|
+
|
||||||
|
@end
|
||||||
|
|
||||||
|
NS_ASSUME_NONNULL_END
|
||||||
|
diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTScrollViewComponentView.mm
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++ (BOOL)shouldBeRecycled
|
||||||
|
+{
|
||||||
|
+ return NO;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
@end
|
||||||
|
|
||||||
|
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
|
||||||
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||||
index e9b330f..ec5f58c 100644
|
index e9b330f..ec5f58c 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
|
||||||
@@ -15,7 +44,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
|||||||
index 53bfd04..ff1b1ed 100644
|
index 53bfd04..ff1b1ed 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
|
||||||
@@ -23,6 +23,7 @@
|
@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
|
||||||
UIColor *_titleColor;
|
UIColor *_titleColor;
|
||||||
CGFloat _progressViewOffset;
|
CGFloat _progressViewOffset;
|
||||||
BOOL _hasMovedToWindow;
|
BOOL _hasMovedToWindow;
|
||||||
@@ -23,7 +52,7 @@ index 53bfd04..ff1b1ed 100644
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (instancetype)init
|
- (instancetype)init
|
||||||
@@ -58,6 +59,12 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
@@ -58,6 +59,12 @@ - (void)layoutSubviews
|
||||||
_isInitialRender = false;
|
_isInitialRender = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +65,7 @@ index 53bfd04..ff1b1ed 100644
|
|||||||
- (void)didMoveToWindow
|
- (void)didMoveToWindow
|
||||||
{
|
{
|
||||||
[super didMoveToWindow];
|
[super didMoveToWindow];
|
||||||
@@ -221,4 +228,50 @@ RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : (NSCoder *)aDecoder)
|
@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +120,7 @@ diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshCont
|
|||||||
index 40aaf9c..1c60164 100644
|
index 40aaf9c..1c60164 100644
|
||||||
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||||
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
|
||||||
@@ -22,11 +22,12 @@ RCT_EXPORT_MODULE()
|
@@ -22,11 +22,12 @@ - (UIView *)view
|
||||||
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
|
||||||
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
|
||||||
@@ -105,15 +134,3 @@ index 40aaf9c..1c60164 100644
|
|||||||
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
|
||||||
{
|
{
|
||||||
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||||
diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
|
||||||
index cd1e7eb..c1d0172 100644
|
|
||||||
--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
|
||||||
+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollViewManager.m
|
|
||||||
@@ -83,6 +83,7 @@ RCT_EXPORT_VIEW_PROPERTY(showsVerticalScrollIndicator, BOOL)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(scrollEventThrottle, NSTimeInterval)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(zoomScale, CGFloat)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
|
|
||||||
+RCT_EXPORT_VIEW_PROPERTY(scrollIndicatorInsets, UIEdgeInsets)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(verticalScrollIndicatorInsets, UIEdgeInsets)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(scrollToOverflowEnabled, BOOL)
|
|
||||||
RCT_EXPORT_VIEW_PROPERTY(snapToInterval, int)
|
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
import {InteractionManager, View} from 'react-native'
|
import {InteractionManager, View} from 'react-native'
|
||||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
import {
|
||||||
|
type AnimatedRef,
|
||||||
|
measure,
|
||||||
|
type MeasuredDimensions,
|
||||||
|
runOnJS,
|
||||||
|
runOnUI,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
|
|
||||||
import {HandleRef, measureHandle} from '#/lib/hooks/useHandleRef'
|
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
import {useLightboxControls} from '#/state/lightbox'
|
||||||
import {Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types'
|
||||||
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage'
|
||||||
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {EmbedType} from '#/types/bsky/post'
|
import {type EmbedType} from '#/types/bsky/post'
|
||||||
import {CommonProps} from './types'
|
import {type CommonProps} from './types'
|
||||||
|
|
||||||
export function ImageEmbed({
|
export function ImageEmbed({
|
||||||
embed,
|
embed,
|
||||||
@@ -45,13 +50,15 @@ export function ImageEmbed({
|
|||||||
}
|
}
|
||||||
const onPress = (
|
const onPress = (
|
||||||
index: number,
|
index: number,
|
||||||
refs: HandleRef[],
|
refs: AnimatedRef<any>[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => {
|
) => {
|
||||||
const handles = refs.map(r => r.current)
|
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rects = handles.map(measureHandle)
|
const rects: (MeasuredDimensions | null)[] = []
|
||||||
|
for (const r of refs) {
|
||||||
|
rects.push(measure(r))
|
||||||
|
}
|
||||||
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
runOnJS(_openLightbox)(index, rects, fetchedDims)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {
|
import {
|
||||||
AccessibilityProps,
|
type AccessibilityProps,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
TextInput,
|
TextInput,
|
||||||
TextInputProps,
|
type TextInputProps,
|
||||||
TextStyle,
|
type TextStyle,
|
||||||
View,
|
View,
|
||||||
ViewStyle,
|
type ViewStyle,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
@@ -16,13 +16,13 @@ import {
|
|||||||
applyFonts,
|
applyFonts,
|
||||||
atoms as a,
|
atoms as a,
|
||||||
ios,
|
ios,
|
||||||
TextStyleProp,
|
type TextStyleProp,
|
||||||
useAlf,
|
useAlf,
|
||||||
useTheme,
|
useTheme,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {Props as SVGIconProps} from '#/components/icons/common'
|
import {type Props as SVGIconProps} from '#/components/icons/common'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
const Context = React.createContext<{
|
const Context = React.createContext<{
|
||||||
@@ -196,7 +196,7 @@ export function createInput(Component: typeof TextInput) {
|
|||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
},
|
},
|
||||||
ios({paddingTop: 12, paddingBottom: 13}),
|
ios({paddingTop: 12, paddingBottom: 13}),
|
||||||
android(a.py_sm),
|
android(a.py_md),
|
||||||
// fix for autofill styles covering border
|
// fix for autofill styles covering border
|
||||||
web({
|
web({
|
||||||
paddingTop: 10,
|
paddingTop: 10,
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
import {useState} from 'react'
|
|
||||||
import {AnimatedRef, measure, MeasuredDimensions} from 'react-native-reanimated'
|
|
||||||
|
|
||||||
export type HandleRef = {
|
|
||||||
(node: any): void
|
|
||||||
current: null | number
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is a lighterweight alternative to `useAnimatedRef()` for imperative UI thread actions.
|
|
||||||
// Render it like <View ref={ref} />, then pass `ref.current` to `measureHandle()` and such.
|
|
||||||
export function useHandleRef(): HandleRef {
|
|
||||||
return useState(() => {
|
|
||||||
const ref = (node: any) => {
|
|
||||||
if (node) {
|
|
||||||
ref.current =
|
|
||||||
node._nativeTag ??
|
|
||||||
node.__nativeTag ??
|
|
||||||
node.canonical?.nativeTag ??
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
ref.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ref.current = null
|
|
||||||
return ref
|
|
||||||
})[0] as HandleRef
|
|
||||||
}
|
|
||||||
|
|
||||||
// When using this version, you need to read ref.current on the JS thread, and pass it to UI.
|
|
||||||
export function measureHandle(
|
|
||||||
current: number | null,
|
|
||||||
): MeasuredDimensions | null {
|
|
||||||
'worklet'
|
|
||||||
if (current !== null) {
|
|
||||||
return measure((() => current) as AnimatedRef<any>)
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+37
-50
@@ -1,8 +1,9 @@
|
|||||||
import {Image as RNImage, Share as RNShare} from 'react-native'
|
import {Image as RNImage} from 'react-native'
|
||||||
import uuid from 'react-native-uuid'
|
import uuid from 'react-native-uuid'
|
||||||
import {
|
import {
|
||||||
cacheDirectory,
|
cacheDirectory,
|
||||||
copyAsync,
|
copyAsync,
|
||||||
|
createDownloadResumable,
|
||||||
deleteAsync,
|
deleteAsync,
|
||||||
EncodingType,
|
EncodingType,
|
||||||
getInfoAsync,
|
getInfoAsync,
|
||||||
@@ -14,7 +15,6 @@ import {manipulateAsync, SaveFormat} from 'expo-image-manipulator'
|
|||||||
import * as MediaLibrary from 'expo-media-library'
|
import * as MediaLibrary from 'expo-media-library'
|
||||||
import * as Sharing from 'expo-sharing'
|
import * as Sharing from 'expo-sharing'
|
||||||
import {Buffer} from 'buffer'
|
import {Buffer} from 'buffer'
|
||||||
import RNFetchBlob from 'rn-fetch-blob'
|
|
||||||
|
|
||||||
import {POST_IMG_MAX} from '#/lib/constants'
|
import {POST_IMG_MAX} from '#/lib/constants'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
@@ -68,28 +68,13 @@ export async function downloadAndResize(opts: DownloadAndResizeOpts) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let downloadRes
|
const path = createPath(appendExt)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadResPromise = RNFetchBlob.config({
|
await downloadImage(opts.uri, path, opts.timeout)
|
||||||
fileCache: true,
|
return await doResize(path, opts)
|
||||||
appendExt,
|
|
||||||
}).fetch('GET', opts.uri)
|
|
||||||
const to1 = setTimeout(() => downloadResPromise.cancel(), opts.timeout)
|
|
||||||
downloadRes = await downloadResPromise
|
|
||||||
clearTimeout(to1)
|
|
||||||
|
|
||||||
const status = downloadRes.info().status
|
|
||||||
if (status !== 200) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const localUri = normalizePath(downloadRes.path(), true)
|
|
||||||
return await doResize(localUri, opts)
|
|
||||||
} finally {
|
} finally {
|
||||||
// TODO Whenever we remove `rn-fetch-blob`, we will need to replace this `flush()` with a `deleteAsync()` -hailey
|
safeDeleteAsync(path)
|
||||||
if (downloadRes) {
|
|
||||||
downloadRes.flush()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,32 +83,16 @@ export async function shareImageModal({uri}: {uri: string}) {
|
|||||||
// TODO might need to give an error to the user in this case -prf
|
// TODO might need to give an error to the user in this case -prf
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const downloadResponse = await RNFetchBlob.config({
|
|
||||||
fileCache: true,
|
|
||||||
}).fetch('GET', uri)
|
|
||||||
|
|
||||||
// NOTE
|
|
||||||
// assuming PNG
|
|
||||||
// we're currently relying on the fact our CDN only serves pngs
|
// we're currently relying on the fact our CDN only serves pngs
|
||||||
// -prf
|
// -prf
|
||||||
|
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||||
let imagePath = downloadResponse.path()
|
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
safeDeleteAsync(imageUri)
|
||||||
|
await Sharing.shareAsync(imagePath, {
|
||||||
// NOTE
|
mimeType: 'image/png',
|
||||||
// for some reason expo-sharing refuses to work on iOS
|
UTI: 'image/png',
|
||||||
// ...and visa versa
|
})
|
||||||
// -prf
|
|
||||||
if (isIOS) {
|
|
||||||
await RNShare.share({url: imagePath})
|
|
||||||
} else {
|
|
||||||
await Sharing.shareAsync(imagePath, {
|
|
||||||
mimeType: 'image/png',
|
|
||||||
UTI: 'image/png',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
safeDeleteAsync(imagePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ALBUM_NAME = 'Bluesky'
|
const ALBUM_NAME = 'Bluesky'
|
||||||
@@ -134,11 +103,8 @@ export async function saveImageToMediaLibrary({uri}: {uri: string}) {
|
|||||||
// assuming PNG
|
// assuming PNG
|
||||||
// we're currently relying on the fact our CDN only serves pngs
|
// we're currently relying on the fact our CDN only serves pngs
|
||||||
// -prf
|
// -prf
|
||||||
const downloadResponse = await RNFetchBlob.config({
|
const imageUri = await downloadImage(uri, createPath('png'), 5e3)
|
||||||
fileCache: true,
|
const imagePath = await moveToPermanentPath(imageUri, '.png')
|
||||||
}).fetch('GET', uri)
|
|
||||||
let imagePath = downloadResponse.path()
|
|
||||||
imagePath = normalizePath(await moveToPermanentPath(imagePath, '.png'), true)
|
|
||||||
|
|
||||||
// save
|
// save
|
||||||
try {
|
try {
|
||||||
@@ -403,3 +369,24 @@ export function getResizedDimensions(originalDims: {
|
|||||||
height: Math.round(originalDims.height * ratio),
|
height: Math.round(originalDims.height * ratio),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createPath(ext: string) {
|
||||||
|
// cacheDirectory will never be null on native, so the null check here is not necessary except for typescript.
|
||||||
|
// we use a web-only function for downloadAndResize on web
|
||||||
|
return `${cacheDirectory ?? ''}/${uuid.v4()}.${ext}`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadImage(uri: string, path: string, timeout: number) {
|
||||||
|
const dlResumable = createDownloadResumable(uri, path, {cache: true})
|
||||||
|
|
||||||
|
const to1 = setTimeout(() => dlResumable.cancelAsync(), timeout)
|
||||||
|
|
||||||
|
const dlRes = await dlResumable.downloadAsync()
|
||||||
|
clearTimeout(to1)
|
||||||
|
|
||||||
|
if (!dlRes?.uri) {
|
||||||
|
throw new Error('Failed to download image - dlRes is undefined')
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizePath(dlRes.uri)
|
||||||
|
}
|
||||||
|
|||||||
+253
-155
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
|||||||
import React, {memo, useEffect} from 'react'
|
import {memo, useCallback, useEffect, useMemo} from 'react'
|
||||||
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native'
|
||||||
import {
|
import Animated, {
|
||||||
|
measure,
|
||||||
type MeasuredDimensions,
|
type MeasuredDimensions,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
runOnUI,
|
runOnUI,
|
||||||
|
useAnimatedRef,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'
|
||||||
@@ -14,7 +16,6 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
import {useActorStatus} from '#/lib/actor-status'
|
import {useActorStatus} from '#/lib/actor-status'
|
||||||
import {BACK_HITSLOP} from '#/lib/constants'
|
import {BACK_HITSLOP} from '#/lib/constants'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
|
||||||
import {type NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
@@ -59,9 +60,9 @@ let ProfileHeaderShell = ({
|
|||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const liveStatusControl = useDialogControl()
|
const liveStatusControl = useDialogControl()
|
||||||
|
|
||||||
const aviRef = useHandleRef()
|
const aviRef = useAnimatedRef()
|
||||||
|
|
||||||
const onPressBack = React.useCallback(() => {
|
const onPressBack = useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
} else {
|
} else {
|
||||||
@@ -69,7 +70,7 @@ let ProfileHeaderShell = ({
|
|||||||
}
|
}
|
||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
const _openLightbox = React.useCallback(
|
const _openLightbox = useCallback(
|
||||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||||
openLightbox({
|
openLightbox({
|
||||||
images: [
|
images: [
|
||||||
@@ -92,7 +93,7 @@ let ProfileHeaderShell = ({
|
|||||||
[openLightbox],
|
[openLightbox],
|
||||||
)
|
)
|
||||||
|
|
||||||
const isMe = React.useMemo(
|
const isMe = useMemo(
|
||||||
() => currentAccount?.did === profile.did,
|
() => currentAccount?.did === profile.did,
|
||||||
[currentAccount, profile],
|
[currentAccount, profile],
|
||||||
)
|
)
|
||||||
@@ -109,7 +110,7 @@ let ProfileHeaderShell = ({
|
|||||||
}
|
}
|
||||||
}, [live.isActive, profile.did])
|
}, [live.isActive, profile.did])
|
||||||
|
|
||||||
const onPressAvi = React.useCallback(() => {
|
const onPressAvi = useCallback(() => {
|
||||||
if (live.isActive) {
|
if (live.isActive) {
|
||||||
playHaptic('Light')
|
playHaptic('Light')
|
||||||
logger.metric(
|
logger.metric(
|
||||||
@@ -122,10 +123,9 @@ let ProfileHeaderShell = ({
|
|||||||
const modui = moderation.ui('avatar')
|
const modui = moderation.ui('avatar')
|
||||||
const avatar = profile.avatar
|
const avatar = profile.avatar
|
||||||
if (avatar && !(modui.blur && modui.noOverride)) {
|
if (avatar && !(modui.blur && modui.noOverride)) {
|
||||||
const aviHandle = aviRef.current
|
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measureHandle(aviHandle)
|
const rect = measure(aviRef)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ let ProfileHeaderShell = ({
|
|||||||
styles.avi,
|
styles.avi,
|
||||||
profile.associated?.labeler && styles.aviLabeler,
|
profile.associated?.labeler && styles.aviLabeler,
|
||||||
]}>
|
]}>
|
||||||
<View ref={aviRef} collapsable={false}>
|
<Animated.View ref={aviRef} collapsable={false}>
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
size={live.isActive ? 88 : 90}
|
size={live.isActive ? 88 : 90}
|
||||||
@@ -231,7 +231,7 @@ let ProfileHeaderShell = ({
|
|||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
/>
|
/>
|
||||||
{live.isActive && <LiveIndicator size="large" />}
|
{live.isActive && <LiveIndicator size="large" />}
|
||||||
</View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</TouchableWithoutFeedback>
|
</TouchableWithoutFeedback>
|
||||||
</GrowableAvatar>
|
</GrowableAvatar>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ const consumedSources = new Map<string, PostSource>()
|
|||||||
* Used for FeedFeedback and other ephemeral non-critical systems.
|
* Used for FeedFeedback and other ephemeral non-critical systems.
|
||||||
*/
|
*/
|
||||||
export function setUnstablePostSource(key: string, source: PostSource) {
|
export function setUnstablePostSource(key: string, source: PostSource) {
|
||||||
assertValid(
|
assertValidDevOnly(
|
||||||
key,
|
key,
|
||||||
`setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`,
|
`setUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`,
|
||||||
)
|
)
|
||||||
@@ -51,9 +51,10 @@ export function setUnstablePostSource(key: string, source: PostSource) {
|
|||||||
export function useUnstablePostSource(key: string) {
|
export function useUnstablePostSource(key: string) {
|
||||||
const id = useId()
|
const id = useId()
|
||||||
const [source] = useState(() => {
|
const [source] = useState(() => {
|
||||||
assertValid(
|
assertValidDevOnly(
|
||||||
key,
|
key,
|
||||||
`consumeUnstablePostSource key should be a URI containing a handle, received ${key} — use buildPostSourceKey`,
|
`consumeUnstablePostSource key should be a URI containing a handle, received ${key} — be sure to use buildPostSourceKey when setting the source`,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
const source = consumedSources.get(id) || transientSources.get(key)
|
const source = consumedSources.get(id) || transientSources.get(key)
|
||||||
if (source) {
|
if (source) {
|
||||||
@@ -87,11 +88,15 @@ export function buildPostSourceKey(key: string, handle: string) {
|
|||||||
/**
|
/**
|
||||||
* Just a lil dev helper
|
* Just a lil dev helper
|
||||||
*/
|
*/
|
||||||
function assertValid(key: string, message: string) {
|
function assertValidDevOnly(key: string, message: string, beChill = false) {
|
||||||
if (__DEV__) {
|
if (__DEV__) {
|
||||||
const urip = new AtUri(key)
|
const urip = new AtUri(key)
|
||||||
if (urip.host.startsWith('did:')) {
|
if (urip.host.startsWith('did:')) {
|
||||||
throw new Error(message)
|
if (beChill) {
|
||||||
|
logger.warn(message)
|
||||||
|
} else {
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ import {
|
|||||||
type SupportedMimeTypes,
|
type SupportedMimeTypes,
|
||||||
} from '#/lib/constants'
|
} from '#/lib/constants'
|
||||||
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
|
||||||
|
import {useAppState} from '#/lib/hooks/useAppState'
|
||||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
@@ -823,6 +824,8 @@ let ComposerPost = React.memo(function ComposerPost({
|
|||||||
[post.id, onSelectVideo, onImageAdd, _],
|
[post.id, onSelectVideo, onImageAdd, _],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useHideKeyboardOnBackground()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
@@ -1527,6 +1530,18 @@ function isEmptyPost(post: PostDraft) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useHideKeyboardOnBackground() {
|
||||||
|
const appState = useAppState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isIOS) {
|
||||||
|
if (appState === 'inactive') {
|
||||||
|
Keyboard.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [appState])
|
||||||
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
topbarInner: {
|
topbarInner: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
import {AppBskyRichtextFacet, RichText} from '@atproto/api'
|
||||||
import PasteInput, {
|
import PasteInput, {
|
||||||
type PastedFile,
|
type PastedFile,
|
||||||
type PasteInputRef,
|
type PasteInputRef, // @ts-expect-error no types when installing from github
|
||||||
} from '@mattermost/react-native-paste-input'
|
} from '@mattermost/react-native-paste-input'
|
||||||
|
|
||||||
import {POST_IMG_MAX} from '#/lib/constants'
|
import {POST_IMG_MAX} from '#/lib/constants'
|
||||||
|
|||||||
+115
-107
@@ -1,16 +1,22 @@
|
|||||||
import React, {forwardRef, useCallback, useContext} from 'react'
|
import {
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
import {DrawerGestureContext} from 'react-native-drawer-layout'
|
||||||
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
import {Gesture, GestureDetector} from 'react-native-gesture-handler'
|
||||||
import PagerView, {
|
import PagerView, {
|
||||||
PagerViewOnPageScrollEventData,
|
type PagerViewOnPageScrollEventData,
|
||||||
PagerViewOnPageSelectedEvent,
|
type PagerViewOnPageSelectedEvent,
|
||||||
PagerViewOnPageSelectedEventData,
|
type PagerViewOnPageSelectedEventData,
|
||||||
PageScrollStateChangedNativeEventData,
|
type PageScrollStateChangedNativeEventData,
|
||||||
} from 'react-native-pager-view'
|
} from 'react-native-pager-view'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
runOnJS,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useEvent,
|
useEvent,
|
||||||
useHandler,
|
useHandler,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -36,8 +42,12 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
|
// tab pressed, yet to scroll to page
|
||||||
|
onTabPressed?: (index: number) => void
|
||||||
|
// scroll settled
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
onPageScrollStateChanged?: (
|
onPageScrollStateChanged?: (
|
||||||
scrollState: 'idle' | 'dragging' | 'settling',
|
scrollState: 'idle' | 'dragging' | 'settling',
|
||||||
@@ -47,114 +57,112 @@ interface Props {
|
|||||||
|
|
||||||
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
const AnimatedPagerView = Animated.createAnimatedComponent(PagerView)
|
||||||
|
|
||||||
export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>(
|
export function Pager({
|
||||||
function PagerImpl(
|
ref,
|
||||||
|
children,
|
||||||
|
initialPage = 0,
|
||||||
|
renderTabBar,
|
||||||
|
onPageSelected: parentOnPageSelected,
|
||||||
|
onTabPressed: parentOnTabPressed,
|
||||||
|
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
||||||
|
testID,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
|
const pagerView = useRef<PagerView>(null)
|
||||||
|
|
||||||
|
const [isIdle, setIsIdle] = useState(true)
|
||||||
|
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
const canSwipeDrawer = selectedPage === 0 && isIdle
|
||||||
|
setDrawerSwipeDisabled(!canSwipeDrawer)
|
||||||
|
return () => {
|
||||||
|
setDrawerSwipeDisabled(false)
|
||||||
|
}
|
||||||
|
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
||||||
|
)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setPage: (index: number) => {
|
||||||
|
pagerView.current?.setPage(index)
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const onPageSelectedJSThread = useCallback(
|
||||||
|
(nextPosition: number) => {
|
||||||
|
setSelectedPage(nextPosition)
|
||||||
|
parentOnPageSelected?.(nextPosition)
|
||||||
|
},
|
||||||
|
[setSelectedPage, parentOnPageSelected],
|
||||||
|
)
|
||||||
|
|
||||||
|
const onTabBarSelect = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
parentOnTabPressed?.(index)
|
||||||
|
pagerView.current?.setPage(index)
|
||||||
|
},
|
||||||
|
[pagerView, parentOnTabPressed],
|
||||||
|
)
|
||||||
|
|
||||||
|
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
||||||
|
const dragProgress = useSharedValue(selectedPage)
|
||||||
|
const didInit = useSharedValue(false)
|
||||||
|
const handlePageScroll = usePagerHandlers(
|
||||||
{
|
{
|
||||||
children,
|
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
||||||
initialPage = 0,
|
'worklet'
|
||||||
renderTabBar,
|
if (didInit.get() === false) {
|
||||||
onPageScrollStateChanged: parentOnPageScrollStateChanged,
|
// On iOS, there's a spurious scroll event with 0 position
|
||||||
onPageSelected: parentOnPageSelected,
|
// even if a different page was supplied as the initial page.
|
||||||
testID,
|
// Ignore it and wait for the first confirmed selection instead.
|
||||||
}: React.PropsWithChildren<Props>,
|
return
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
const pagerView = React.useRef<PagerView>(null)
|
|
||||||
|
|
||||||
const [isIdle, setIsIdle] = React.useState(true)
|
|
||||||
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
|
|
||||||
useFocusEffect(
|
|
||||||
useCallback(() => {
|
|
||||||
const canSwipeDrawer = selectedPage === 0 && isIdle
|
|
||||||
setDrawerSwipeDisabled(!canSwipeDrawer)
|
|
||||||
return () => {
|
|
||||||
setDrawerSwipeDisabled(false)
|
|
||||||
}
|
}
|
||||||
}, [setDrawerSwipeDisabled, selectedPage, isIdle]),
|
dragProgress.set(e.offset + e.position)
|
||||||
)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
|
||||||
setPage: (index: number) => {
|
|
||||||
pagerView.current?.setPage(index)
|
|
||||||
},
|
},
|
||||||
}))
|
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
||||||
|
'worklet'
|
||||||
const onPageSelectedJSThread = React.useCallback(
|
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
||||||
(nextPosition: number) => {
|
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
||||||
setSelectedPage(nextPosition)
|
// This is a programmatic scroll on Android.
|
||||||
parentOnPageSelected?.(nextPosition)
|
// Stay "idle" to match iOS and avoid confusing downstream code.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dragState.set(e.pageScrollState)
|
||||||
|
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
||||||
},
|
},
|
||||||
[setSelectedPage, parentOnPageSelected],
|
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
||||||
)
|
'worklet'
|
||||||
|
didInit.set(true)
|
||||||
const onTabBarSelect = React.useCallback(
|
runOnJS(onPageSelectedJSThread)(e.position)
|
||||||
(index: number) => {
|
|
||||||
pagerView.current?.setPage(index)
|
|
||||||
},
|
},
|
||||||
[pagerView],
|
},
|
||||||
)
|
[parentOnPageScrollStateChanged],
|
||||||
|
)
|
||||||
|
|
||||||
const dragState = useSharedValue<'idle' | 'settling' | 'dragging'>('idle')
|
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
||||||
const dragProgress = useSharedValue(selectedPage)
|
const nativeGesture =
|
||||||
const didInit = useSharedValue(false)
|
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
||||||
const handlePageScroll = usePagerHandlers(
|
|
||||||
{
|
|
||||||
onPageScroll(e: PagerViewOnPageScrollEventData) {
|
|
||||||
'worklet'
|
|
||||||
if (didInit.get() === false) {
|
|
||||||
// On iOS, there's a spurious scroll event with 0 position
|
|
||||||
// even if a different page was supplied as the initial page.
|
|
||||||
// Ignore it and wait for the first confirmed selection instead.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dragProgress.set(e.offset + e.position)
|
|
||||||
},
|
|
||||||
onPageScrollStateChanged(e: PageScrollStateChangedNativeEventData) {
|
|
||||||
'worklet'
|
|
||||||
runOnJS(setIsIdle)(e.pageScrollState === 'idle')
|
|
||||||
if (dragState.get() === 'idle' && e.pageScrollState === 'settling') {
|
|
||||||
// This is a programmatic scroll on Android.
|
|
||||||
// Stay "idle" to match iOS and avoid confusing downstream code.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
dragState.set(e.pageScrollState)
|
|
||||||
parentOnPageScrollStateChanged?.(e.pageScrollState)
|
|
||||||
},
|
|
||||||
onPageSelected(e: PagerViewOnPageSelectedEventData) {
|
|
||||||
'worklet'
|
|
||||||
didInit.set(true)
|
|
||||||
runOnJS(onPageSelectedJSThread)(e.position)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
[parentOnPageScrollStateChanged],
|
|
||||||
)
|
|
||||||
|
|
||||||
const drawerGesture = useContext(DrawerGestureContext) ?? Gesture.Native() // noop for web
|
return (
|
||||||
const nativeGesture =
|
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
||||||
Gesture.Native().requireExternalGestureToFail(drawerGesture)
|
{renderTabBar({
|
||||||
|
selectedPage,
|
||||||
return (
|
onSelect: onTabBarSelect,
|
||||||
<View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}>
|
dragProgress,
|
||||||
{renderTabBar({
|
dragState,
|
||||||
selectedPage,
|
})}
|
||||||
onSelect: onTabBarSelect,
|
<GestureDetector gesture={nativeGesture}>
|
||||||
dragProgress,
|
<AnimatedPagerView
|
||||||
dragState,
|
ref={pagerView}
|
||||||
})}
|
style={[a.flex_1]}
|
||||||
<GestureDetector gesture={nativeGesture}>
|
initialPage={initialPage}
|
||||||
<AnimatedPagerView
|
onPageScroll={handlePageScroll}>
|
||||||
ref={pagerView}
|
{children}
|
||||||
style={[a.flex_1]}
|
</AnimatedPagerView>
|
||||||
initialPage={initialPage}
|
</GestureDetector>
|
||||||
onPageScroll={handlePageScroll}>
|
</View>
|
||||||
{children}
|
)
|
||||||
</AnimatedPagerView>
|
}
|
||||||
</GestureDetector>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function usePagerHandlers(
|
function usePagerHandlers(
|
||||||
handlers: {
|
handlers: {
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import React from 'react'
|
import {
|
||||||
|
Children,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {flushSync} from 'react-dom'
|
import {flushSync} from 'react-dom'
|
||||||
|
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
|
import {atoms as a} from '#/alf'
|
||||||
|
|
||||||
|
export interface PagerRef {
|
||||||
|
setPage: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
export interface RenderTabBarFnProps {
|
export interface RenderTabBarFnProps {
|
||||||
selectedPage: number
|
selectedPage: number
|
||||||
@@ -12,30 +23,30 @@ export interface RenderTabBarFnProps {
|
|||||||
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
export type RenderTabBarFn = (props: RenderTabBarFnProps) => JSX.Element
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
initialPage?: number
|
initialPage?: number
|
||||||
renderTabBar: RenderTabBarFn
|
renderTabBar: RenderTabBarFn
|
||||||
onPageSelected?: (index: number) => void
|
onPageSelected?: (index: number) => void
|
||||||
}
|
}
|
||||||
export const Pager = React.forwardRef(function PagerImpl(
|
|
||||||
{
|
|
||||||
children,
|
|
||||||
initialPage = 0,
|
|
||||||
renderTabBar,
|
|
||||||
onPageSelected,
|
|
||||||
}: React.PropsWithChildren<Props>,
|
|
||||||
ref,
|
|
||||||
) {
|
|
||||||
const [selectedPage, setSelectedPage] = React.useState(initialPage)
|
|
||||||
const scrollYs = React.useRef<Array<number | null>>([])
|
|
||||||
const anchorRef = React.useRef(null)
|
|
||||||
|
|
||||||
React.useImperativeHandle(ref, () => ({
|
export function Pager({
|
||||||
|
ref,
|
||||||
|
children,
|
||||||
|
initialPage = 0,
|
||||||
|
renderTabBar,
|
||||||
|
onPageSelected,
|
||||||
|
}: React.PropsWithChildren<Props>) {
|
||||||
|
const [selectedPage, setSelectedPage] = useState(initialPage)
|
||||||
|
const scrollYs = useRef<Array<number | null>>([])
|
||||||
|
const anchorRef = useRef(null)
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
setPage: (index: number) => {
|
setPage: (index: number) => {
|
||||||
onTabBarSelect(index)
|
onTabBarSelect(index)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const onTabBarSelect = React.useCallback(
|
const onTabBarSelect = useCallback(
|
||||||
(index: number) => {
|
(index: number) => {
|
||||||
const scrollY = window.scrollY
|
const scrollY = window.scrollY
|
||||||
// We want to determine if the tabbar is already "sticking" at the top (in which
|
// We want to determine if the tabbar is already "sticking" at the top (in which
|
||||||
@@ -75,11 +86,13 @@ export const Pager = React.forwardRef(function PagerImpl(
|
|||||||
tabBarAnchor: <View ref={anchorRef} />,
|
tabBarAnchor: <View ref={anchorRef} />,
|
||||||
onSelect: e => onTabBarSelect(e),
|
onSelect: e => onTabBarSelect(e),
|
||||||
})}
|
})}
|
||||||
{React.Children.map(children, (child, i) => (
|
{Children.map(children, (child, i) => (
|
||||||
<View style={selectedPage === i ? s.flex1 : s.hidden} key={`page-${i}`}>
|
<View
|
||||||
|
style={selectedPage === i ? a.flex_1 : a.hidden}
|
||||||
|
key={`page-${i}`}>
|
||||||
{child}
|
{child}
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import * as React from 'react'
|
import {memo, useCallback, useEffect, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
ScrollView,
|
type ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
AnimatedRef,
|
type AnimatedRef,
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
runOnUI,
|
||||||
scrollTo,
|
scrollTo,
|
||||||
SharedValue,
|
type SharedValue,
|
||||||
useAnimatedRef,
|
useAnimatedRef,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -20,9 +19,13 @@ import Animated, {
|
|||||||
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
||||||
import {ScrollProvider} from '#/lib/ScrollContext'
|
import {ScrollProvider} from '#/lib/ScrollContext'
|
||||||
import {isIOS} from '#/platform/detection'
|
import {isIOS} from '#/platform/detection'
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from '#/view/com/pager/Pager'
|
import {
|
||||||
|
Pager,
|
||||||
|
type PagerRef,
|
||||||
|
type RenderTabBarFnProps,
|
||||||
|
} from '#/view/com/pager/Pager'
|
||||||
import {useTheme} from '#/alf'
|
import {useTheme} from '#/alf'
|
||||||
import {ListMethods} from '../util/List'
|
import {type ListMethods} from '../util/List'
|
||||||
import {PagerHeaderProvider} from './PagerHeaderContext'
|
import {PagerHeaderProvider} from './PagerHeaderContext'
|
||||||
import {TabBar} from './TabBar'
|
import {TabBar} from './TabBar'
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ export interface PagerWithHeaderChildParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PagerWithHeaderProps {
|
export interface PagerWithHeaderProps {
|
||||||
|
ref?: React.Ref<PagerRef>
|
||||||
testID?: string
|
testID?: string
|
||||||
children:
|
children:
|
||||||
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
| (((props: PagerWithHeaderChildParams) => JSX.Element) | null)[]
|
||||||
@@ -49,97 +53,94 @@ export interface PagerWithHeaderProps {
|
|||||||
onCurrentPageSelected?: (index: number) => void
|
onCurrentPageSelected?: (index: number) => void
|
||||||
allowHeaderOverScroll?: boolean
|
allowHeaderOverScroll?: boolean
|
||||||
}
|
}
|
||||||
export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
export function PagerWithHeader({
|
||||||
function PageWithHeaderImpl(
|
ref,
|
||||||
{
|
children,
|
||||||
children,
|
testID,
|
||||||
testID,
|
items,
|
||||||
|
isHeaderReady,
|
||||||
|
renderHeader,
|
||||||
|
initialPage,
|
||||||
|
onPageSelected,
|
||||||
|
onCurrentPageSelected,
|
||||||
|
allowHeaderOverScroll,
|
||||||
|
}: PagerWithHeaderProps) {
|
||||||
|
const [currentPage, setCurrentPage] = useState(0)
|
||||||
|
const [tabBarHeight, setTabBarHeight] = useState(0)
|
||||||
|
const [headerOnlyHeight, setHeaderOnlyHeight] = useState(0)
|
||||||
|
const scrollY = useSharedValue(0)
|
||||||
|
const headerHeight = headerOnlyHeight + tabBarHeight
|
||||||
|
|
||||||
|
// capture the header bar sizing
|
||||||
|
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
||||||
|
const height = evt.nativeEvent.layout.height
|
||||||
|
if (height > 0) {
|
||||||
|
// The rounding is necessary to prevent jumps on iOS
|
||||||
|
setTabBarHeight(Math.round(height * 2) / 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
||||||
|
if (height > 0) {
|
||||||
|
// The rounding is necessary to prevent jumps on iOS
|
||||||
|
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const renderTabBar = useCallback(
|
||||||
|
(props: RenderTabBarFnProps) => {
|
||||||
|
return (
|
||||||
|
<PagerHeaderProvider scrollY={scrollY} headerHeight={headerOnlyHeight}>
|
||||||
|
<PagerTabBar
|
||||||
|
headerOnlyHeight={headerOnlyHeight}
|
||||||
|
items={items}
|
||||||
|
isHeaderReady={isHeaderReady}
|
||||||
|
renderHeader={renderHeader}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onCurrentPageSelected={onCurrentPageSelected}
|
||||||
|
onTabBarLayout={onTabBarLayout}
|
||||||
|
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
||||||
|
onSelect={props.onSelect}
|
||||||
|
scrollY={scrollY}
|
||||||
|
testID={testID}
|
||||||
|
allowHeaderOverScroll={allowHeaderOverScroll}
|
||||||
|
dragProgress={props.dragProgress}
|
||||||
|
dragState={props.dragState}
|
||||||
|
/>
|
||||||
|
</PagerHeaderProvider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[
|
||||||
|
headerOnlyHeight,
|
||||||
items,
|
items,
|
||||||
isHeaderReady,
|
isHeaderReady,
|
||||||
renderHeader,
|
renderHeader,
|
||||||
initialPage,
|
currentPage,
|
||||||
onPageSelected,
|
|
||||||
onCurrentPageSelected,
|
onCurrentPageSelected,
|
||||||
|
onTabBarLayout,
|
||||||
|
onHeaderOnlyLayout,
|
||||||
|
scrollY,
|
||||||
|
testID,
|
||||||
allowHeaderOverScroll,
|
allowHeaderOverScroll,
|
||||||
}: PagerWithHeaderProps,
|
],
|
||||||
ref,
|
)
|
||||||
) {
|
|
||||||
const [currentPage, setCurrentPage] = React.useState(0)
|
|
||||||
const [tabBarHeight, setTabBarHeight] = React.useState(0)
|
|
||||||
const [headerOnlyHeight, setHeaderOnlyHeight] = React.useState(0)
|
|
||||||
const scrollY = useSharedValue(0)
|
|
||||||
const headerHeight = headerOnlyHeight + tabBarHeight
|
|
||||||
|
|
||||||
// capture the header bar sizing
|
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
||||||
const onTabBarLayout = useNonReactiveCallback((evt: LayoutChangeEvent) => {
|
const registerRef = useCallback(
|
||||||
const height = evt.nativeEvent.layout.height
|
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
||||||
if (height > 0) {
|
scrollRefs.modify(refs => {
|
||||||
// The rounding is necessary to prevent jumps on iOS
|
'worklet'
|
||||||
setTabBarHeight(Math.round(height * 2) / 2)
|
refs[atIndex] = scrollRef
|
||||||
}
|
return refs
|
||||||
})
|
})
|
||||||
const onHeaderOnlyLayout = useNonReactiveCallback((height: number) => {
|
},
|
||||||
if (height > 0) {
|
[scrollRefs],
|
||||||
// The rounding is necessary to prevent jumps on iOS
|
)
|
||||||
setHeaderOnlyHeight(Math.round(height * 2) / 2)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const renderTabBar = React.useCallback(
|
const lastForcedScrollY = useSharedValue(0)
|
||||||
(props: RenderTabBarFnProps) => {
|
const adjustScrollForOtherPages = useCallback(
|
||||||
return (
|
(scrollState: 'idle' | 'dragging' | 'settling') => {
|
||||||
<PagerHeaderProvider
|
|
||||||
scrollY={scrollY}
|
|
||||||
headerHeight={headerOnlyHeight}>
|
|
||||||
<PagerTabBar
|
|
||||||
headerOnlyHeight={headerOnlyHeight}
|
|
||||||
items={items}
|
|
||||||
isHeaderReady={isHeaderReady}
|
|
||||||
renderHeader={renderHeader}
|
|
||||||
currentPage={currentPage}
|
|
||||||
onCurrentPageSelected={onCurrentPageSelected}
|
|
||||||
onTabBarLayout={onTabBarLayout}
|
|
||||||
onHeaderOnlyLayout={onHeaderOnlyLayout}
|
|
||||||
onSelect={props.onSelect}
|
|
||||||
scrollY={scrollY}
|
|
||||||
testID={testID}
|
|
||||||
allowHeaderOverScroll={allowHeaderOverScroll}
|
|
||||||
dragProgress={props.dragProgress}
|
|
||||||
dragState={props.dragState}
|
|
||||||
/>
|
|
||||||
</PagerHeaderProvider>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
[
|
|
||||||
headerOnlyHeight,
|
|
||||||
items,
|
|
||||||
isHeaderReady,
|
|
||||||
renderHeader,
|
|
||||||
currentPage,
|
|
||||||
onCurrentPageSelected,
|
|
||||||
onTabBarLayout,
|
|
||||||
onHeaderOnlyLayout,
|
|
||||||
scrollY,
|
|
||||||
testID,
|
|
||||||
allowHeaderOverScroll,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
const scrollRefs = useSharedValue<Array<AnimatedRef<any> | null>>([])
|
|
||||||
const registerRef = React.useCallback(
|
|
||||||
(scrollRef: AnimatedRef<any> | null, atIndex: number) => {
|
|
||||||
scrollRefs.modify(refs => {
|
|
||||||
'worklet'
|
|
||||||
refs[atIndex] = scrollRef
|
|
||||||
return refs
|
|
||||||
})
|
|
||||||
},
|
|
||||||
[scrollRefs],
|
|
||||||
)
|
|
||||||
|
|
||||||
const lastForcedScrollY = useSharedValue(0)
|
|
||||||
const adjustScrollForOtherPages = () => {
|
|
||||||
'worklet'
|
'worklet'
|
||||||
|
if (scrollState !== 'dragging') return
|
||||||
const currentScrollY = scrollY.get()
|
const currentScrollY = scrollY.get()
|
||||||
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
const forcedScrollY = Math.min(currentScrollY, headerOnlyHeight)
|
||||||
if (lastForcedScrollY.get() !== forcedScrollY) {
|
if (lastForcedScrollY.get() !== forcedScrollY) {
|
||||||
@@ -152,75 +153,69 @@ export const PagerWithHeader = React.forwardRef<PagerRef, PagerWithHeaderProps>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[currentPage, headerOnlyHeight, lastForcedScrollY, scrollRefs, scrollY],
|
||||||
|
)
|
||||||
|
|
||||||
const throttleTimeout = React.useRef<ReturnType<typeof setTimeout> | null>(
|
const onScrollWorklet = useCallback(
|
||||||
null,
|
(e: NativeScrollEvent) => {
|
||||||
)
|
'worklet'
|
||||||
const queueThrottledOnScroll = useNonReactiveCallback(() => {
|
const nextScrollY = e.contentOffset.y
|
||||||
if (!throttleTimeout.current) {
|
// HACK: onScroll is reporting some strange values on load (negative header height).
|
||||||
throttleTimeout.current = setTimeout(() => {
|
// Highly improbable that you'd be overscrolled by over 400px -
|
||||||
throttleTimeout.current = null
|
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
||||||
runOnUI(adjustScrollForOtherPages)()
|
const isPossiblyInvalid =
|
||||||
}, 80 /* Sync often enough you're unlikely to catch it unsynced */)
|
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
||||||
|
if (!isPossiblyInvalid) {
|
||||||
|
scrollY.set(nextScrollY)
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
[scrollY, headerHeight],
|
||||||
|
)
|
||||||
|
|
||||||
const onScrollWorklet = React.useCallback(
|
const onPageSelectedInner = useCallback(
|
||||||
(e: NativeScrollEvent) => {
|
(index: number) => {
|
||||||
'worklet'
|
setCurrentPage(index)
|
||||||
const nextScrollY = e.contentOffset.y
|
onPageSelected?.(index)
|
||||||
// HACK: onScroll is reporting some strange values on load (negative header height).
|
},
|
||||||
// Highly improbable that you'd be overscrolled by over 400px -
|
[onPageSelected, setCurrentPage],
|
||||||
// in fact, I actually can't do it, so let's just ignore those. -sfn
|
)
|
||||||
const isPossiblyInvalid =
|
|
||||||
headerHeight > 0 && Math.round(nextScrollY * 2) / 2 === -headerHeight
|
|
||||||
if (!isPossiblyInvalid) {
|
|
||||||
scrollY.set(nextScrollY)
|
|
||||||
runOnJS(queueThrottledOnScroll)()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[scrollY, queueThrottledOnScroll, headerHeight],
|
|
||||||
)
|
|
||||||
|
|
||||||
const onPageSelectedInner = React.useCallback(
|
const onTabPressed = useCallback(() => {
|
||||||
(index: number) => {
|
runOnUI(adjustScrollForOtherPages)('dragging')
|
||||||
setCurrentPage(index)
|
}, [adjustScrollForOtherPages])
|
||||||
onPageSelected?.(index)
|
|
||||||
},
|
|
||||||
[onPageSelected, setCurrentPage],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Pager
|
<Pager
|
||||||
ref={ref}
|
ref={ref}
|
||||||
testID={testID}
|
testID={testID}
|
||||||
initialPage={initialPage}
|
initialPage={initialPage}
|
||||||
onPageSelected={onPageSelectedInner}
|
onTabPressed={onTabPressed}
|
||||||
renderTabBar={renderTabBar}>
|
onPageSelected={onPageSelectedInner}
|
||||||
{toArray(children)
|
renderTabBar={renderTabBar}
|
||||||
.filter(Boolean)
|
onPageScrollStateChanged={adjustScrollForOtherPages}>
|
||||||
.map((child, i) => {
|
{toArray(children)
|
||||||
const isReady =
|
.filter(Boolean)
|
||||||
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
.map((child, i) => {
|
||||||
return (
|
const isReady =
|
||||||
<View key={i} collapsable={false}>
|
isHeaderReady && headerOnlyHeight > 0 && tabBarHeight > 0
|
||||||
<PagerItem
|
return (
|
||||||
headerHeight={headerHeight}
|
<View key={i} collapsable={false}>
|
||||||
index={i}
|
<PagerItem
|
||||||
isReady={isReady}
|
headerHeight={headerHeight}
|
||||||
isFocused={i === currentPage}
|
index={i}
|
||||||
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
isReady={isReady}
|
||||||
registerRef={registerRef}
|
isFocused={i === currentPage}
|
||||||
renderTab={child}
|
onScrollWorklet={i === currentPage ? onScrollWorklet : noop}
|
||||||
/>
|
registerRef={registerRef}
|
||||||
</View>
|
renderTab={child}
|
||||||
)
|
/>
|
||||||
})}
|
</View>
|
||||||
</Pager>
|
)
|
||||||
)
|
})}
|
||||||
},
|
</Pager>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let PagerTabBar = ({
|
let PagerTabBar = ({
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -258,7 +253,7 @@ let PagerTabBar = ({
|
|||||||
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
dragState: SharedValue<'idle' | 'dragging' | 'settling'>
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [minimumHeaderHeight, setMinimumHeaderHeight] = React.useState(0)
|
const [minimumHeaderHeight, setMinimumHeaderHeight] = useState(0)
|
||||||
const headerTransform = useAnimatedStyle(() => {
|
const headerTransform = useAnimatedStyle(() => {
|
||||||
const translateY =
|
const translateY =
|
||||||
Math.min(
|
Math.min(
|
||||||
@@ -275,7 +270,7 @@ let PagerTabBar = ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const headerRef = React.useRef(null)
|
const headerRef = useRef(null)
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
pointerEvents={isIOS ? 'auto' : 'box-none'}
|
||||||
@@ -327,7 +322,7 @@ let PagerTabBar = ({
|
|||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
PagerTabBar = React.memo(PagerTabBar)
|
PagerTabBar = memo(PagerTabBar)
|
||||||
|
|
||||||
function PagerItem({
|
function PagerItem({
|
||||||
headerHeight,
|
headerHeight,
|
||||||
@@ -348,7 +343,7 @@ function PagerItem({
|
|||||||
}) {
|
}) {
|
||||||
const scrollElRef = useAnimatedRef()
|
const scrollElRef = useAnimatedRef()
|
||||||
|
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
registerRef(scrollElRef, index)
|
registerRef(scrollElRef, index)
|
||||||
return () => {
|
return () => {
|
||||||
registerRef(null, index)
|
registerRef(null, index)
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated'
|
import Animated, {
|
||||||
import {AppBskyGraphDefs} from '@atproto/api'
|
measure,
|
||||||
|
type MeasuredDimensions,
|
||||||
|
runOnJS,
|
||||||
|
runOnUI,
|
||||||
|
useAnimatedRef,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {type AppBskyGraphDefs} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef'
|
|
||||||
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 {makeProfileLink} from '#/lib/routes/links'
|
import {makeProfileLink} from '#/lib/routes/links'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {type NavigationProp} from '#/lib/routes/types'
|
||||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||||
import {emitSoftReset} from '#/state/events'
|
import {emitSoftReset} from '#/state/events'
|
||||||
import {useLightboxControls} from '#/state/lightbox'
|
import {useLightboxControls} from '#/state/lightbox'
|
||||||
import {TextLink} from '#/view/com/util/Link'
|
import {TextLink} from '#/view/com/util/Link'
|
||||||
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
|
||||||
import {Text} from '#/view/com/util/text/Text'
|
import {Text} from '#/view/com/util/text/Text'
|
||||||
import {UserAvatar, UserAvatarType} from '#/view/com/util/UserAvatar'
|
import {UserAvatar, type UserAvatarType} from '#/view/com/util/UserAvatar'
|
||||||
import {StarterPack} from '#/components/icons/StarterPack'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
|
||||||
@@ -52,7 +57,7 @@ export function ProfileSubpageHeader({
|
|||||||
const {openLightbox} = useLightboxControls()
|
const {openLightbox} = useLightboxControls()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const canGoBack = navigation.canGoBack()
|
const canGoBack = navigation.canGoBack()
|
||||||
const aviRef = useHandleRef()
|
const aviRef = useAnimatedRef()
|
||||||
|
|
||||||
const _openLightbox = React.useCallback(
|
const _openLightbox = React.useCallback(
|
||||||
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
(uri: string, thumbRect: MeasuredDimensions | null) => {
|
||||||
@@ -81,10 +86,9 @@ export function ProfileSubpageHeader({
|
|||||||
if (
|
if (
|
||||||
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
avatar // TODO && !(view.moderation.avatar.blur && view.moderation.avatar.noOverride)
|
||||||
) {
|
) {
|
||||||
const aviHandle = aviRef.current
|
|
||||||
runOnUI(() => {
|
runOnUI(() => {
|
||||||
'worklet'
|
'worklet'
|
||||||
const rect = measureHandle(aviHandle)
|
const rect = measure(aviRef)
|
||||||
runOnJS(_openLightbox)(avatar, rect)
|
runOnJS(_openLightbox)(avatar, rect)
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
@@ -111,7 +115,7 @@ export function ProfileSubpageHeader({
|
|||||||
paddingBottom: 14,
|
paddingBottom: 14,
|
||||||
paddingHorizontal: isMobile ? 12 : 14,
|
paddingHorizontal: isMobile ? 12 : 14,
|
||||||
}}>
|
}}>
|
||||||
<View ref={aviRef} collapsable={false}>
|
<Animated.View ref={aviRef} collapsable={false}>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="headerAviButton"
|
testID="headerAviButton"
|
||||||
onPress={onPressAvi}
|
onPress={onPressAvi}
|
||||||
@@ -125,7 +129,7 @@ export function ProfileSubpageHeader({
|
|||||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</Animated.View>
|
||||||
<View style={{flex: 1, gap: 4}}>
|
<View style={{flex: 1, gap: 4}}>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<LoadingPlaceholder
|
<LoadingPlaceholder
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
type FontAwesomeIconStyle,
|
type FontAwesomeIconStyle,
|
||||||
type Props as FontAwesomeProps,
|
type Props as FontAwesomeProps,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import type React from 'react'
|
|
||||||
|
|
||||||
const DURATION = 3500
|
const DURATION = 3500
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React, {useRef} from 'react'
|
import React, {useRef} from 'react'
|
||||||
import {DimensionValue, Pressable, View} from 'react-native'
|
import {type DimensionValue, Pressable, View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
type AnimatedRef,
|
||||||
|
useAnimatedRef,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import type {Dimensions} from '#/lib/media/types'
|
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
@@ -68,14 +71,17 @@ export function AutoSizedImage({
|
|||||||
image: AppBskyEmbedImages.ViewImage
|
image: AppBskyEmbedImages.ViewImage
|
||||||
crop?: 'none' | 'square' | 'constrained'
|
crop?: 'none' | 'square' | 'constrained'
|
||||||
hideBadge?: boolean
|
hideBadge?: boolean
|
||||||
onPress?: (containerRef: HandleRef, fetchedDims: Dimensions | null) => void
|
onPress?: (
|
||||||
|
containerRef: AnimatedRef<any>,
|
||||||
|
fetchedDims: Dimensions | null,
|
||||||
|
) => void
|
||||||
onLongPress?: () => void
|
onLongPress?: () => void
|
||||||
onPressIn?: () => void
|
onPressIn?: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const largeAlt = useLargeAltBadgeEnabled()
|
const largeAlt = useLargeAltBadgeEnabled()
|
||||||
const containerRef = useHandleRef()
|
const containerRef = useAnimatedRef()
|
||||||
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
||||||
|
|
||||||
let aspectRatio: number | undefined
|
let aspectRatio: number | undefined
|
||||||
@@ -103,7 +109,7 @@ export function AutoSizedImage({
|
|||||||
const hasAlt = !!image.alt
|
const hasAlt = !!image.alt
|
||||||
|
|
||||||
const contents = (
|
const contents = (
|
||||||
<View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
<Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}>
|
||||||
<Image
|
<Image
|
||||||
contentFit={isContain ? 'contain' : 'cover'}
|
contentFit={isContain ? 'contain' : 'cover'}
|
||||||
style={[a.w_full, a.h_full]}
|
style={[a.w_full, a.h_full]}
|
||||||
@@ -185,7 +191,7 @@ export function AutoSizedImage({
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (cropDisabled) {
|
if (cropDisabled) {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react'
|
import {Pressable, type StyleProp, View, type ViewStyle} from 'react-native'
|
||||||
import {Pressable, StyleProp, View, ViewStyle} from 'react-native'
|
import {type AnimatedRef} from 'react-native-reanimated'
|
||||||
import {Image, ImageStyle} from 'expo-image'
|
import {Image, type ImageStyle} from 'expo-image'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
import type React from 'react'
|
||||||
|
|
||||||
import {HandleRef} from '#/lib/hooks/useHandleRef'
|
import {type Dimensions} from '#/lib/media/types'
|
||||||
import {Dimensions} from '#/lib/media/types'
|
|
||||||
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
import {MediaInsetBorder} from '#/components/MediaInsetBorder'
|
||||||
@@ -20,7 +20,7 @@ interface Props {
|
|||||||
index: number
|
index: number
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: HandleRef[],
|
containerRefs: AnimatedRef<any>[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: EventFunction
|
onLongPress?: EventFunction
|
||||||
@@ -28,7 +28,7 @@ interface Props {
|
|||||||
imageStyle?: StyleProp<ImageStyle>
|
imageStyle?: StyleProp<ImageStyle>
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
insetBorderStyle?: StyleProp<ViewStyle>
|
insetBorderStyle?: StyleProp<ViewStyle>
|
||||||
containerRefs: HandleRef[]
|
containerRefs: AnimatedRef<any>[]
|
||||||
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
|
import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'
|
||||||
import {AppBskyEmbedImages} from '@atproto/api'
|
import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated'
|
||||||
|
import {type AppBskyEmbedImages} from '@atproto/api'
|
||||||
|
|
||||||
import {HandleRef, useHandleRef} from '#/lib/hooks/useHandleRef'
|
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a, useBreakpoints} from '#/alf'
|
||||||
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
import {PostEmbedViewContext} from '#/components/Post/Embed/types'
|
||||||
import {Dimensions} from '../../lightbox/ImageViewing/@types'
|
import {type Dimensions} from '../../lightbox/ImageViewing/@types'
|
||||||
import {GalleryItem} from './Gallery'
|
import {GalleryItem} from './Gallery'
|
||||||
|
|
||||||
interface ImageLayoutGridProps {
|
interface ImageLayoutGridProps {
|
||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: HandleRef[],
|
containerRefs: AnimatedRef<any>[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
@@ -43,7 +43,7 @@ interface ImageLayoutGridInnerProps {
|
|||||||
images: AppBskyEmbedImages.ViewImage[]
|
images: AppBskyEmbedImages.ViewImage[]
|
||||||
onPress?: (
|
onPress?: (
|
||||||
index: number,
|
index: number,
|
||||||
containerRefs: HandleRef[],
|
containerRefs: AnimatedRef<any>[],
|
||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
@@ -56,10 +56,10 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) {
|
|||||||
const gap = props.gap
|
const gap = props.gap
|
||||||
const count = props.images.length
|
const count = props.images.length
|
||||||
|
|
||||||
const containerRef1 = useHandleRef()
|
const containerRef1 = useAnimatedRef()
|
||||||
const containerRef2 = useHandleRef()
|
const containerRef2 = useAnimatedRef()
|
||||||
const containerRef3 = useHandleRef()
|
const containerRef3 = useAnimatedRef()
|
||||||
const containerRef4 = useHandleRef()
|
const containerRef4 = useAnimatedRef()
|
||||||
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
const thumbDimsRef = React.useRef<(Dimensions | null)[]>([])
|
||||||
|
|
||||||
switch (count) {
|
switch (count) {
|
||||||
|
|||||||
Reference in New Issue
Block a user