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:
Eric Bailey
2025-06-12 13:10:23 -05:00
34 changed files with 1293 additions and 1069 deletions
+14 -37
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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,
@@ -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)
+15 -8
View File
@@ -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)
})() })()
} }
+7 -7
View File
@@ -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,
-39
View File
@@ -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
View File
@@ -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)
}
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -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>
+10 -5
View File
@@ -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)
}
} }
} }
} }
+15
View File
@@ -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
View File
@@ -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: {
+31 -18
View File
@@ -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>
) )
}) }
+155 -160
View File
@@ -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)
+14 -10
View File
@@ -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
-1
View File
@@ -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
+14 -8
View File
@@ -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) {
+8 -8
View File
@@ -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)[]>
} }
+10 -10
View File
@@ -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) {
+397 -369
View File
File diff suppressed because it is too large Load Diff