diff --git a/__tests__/usecase/settings/UploadSettings.tsx b/__tests__/usecase/settings/UploadSettings.tsx index 07946eb..d3e2f7b 100644 --- a/__tests__/usecase/settings/UploadSettings.tsx +++ b/__tests__/usecase/settings/UploadSettings.tsx @@ -13,8 +13,6 @@ import { import type { RootStackParamList } from '../../../src/usecase/Root'; import UploadSettings from '../../../src/usecase/settings/UploadSettings'; -jest.mock('../../../src/usecase/shared/UndoTextInput', () => 'TextInput'); - describe('UploadSettings', () => { it('stores the configured settings', async () => { const store = configureStore({ reducer }); diff --git a/app.config.js b/app.config.js index da2b15e..eec7646 100644 --- a/app.config.js +++ b/app.config.js @@ -4,7 +4,7 @@ export default { name: IS_PROD ? 'WeechatRN' : 'WeechatRN (Dev)', description: 'Weechat relay client using websockets', slug: 'WeechatRN', - newArchEnabled: false, + newArchEnabled: true, ios: { bundleIdentifier: IS_PROD ? 'com.matthoran.weechatrn' diff --git a/patches/react-native-reanimated+3.16.6+002+fix-onlayout.patch b/patches/react-native-reanimated+3.16.6+002+fix-onlayout.patch new file mode 100644 index 0000000..764d5a4 --- /dev/null +++ b/patches/react-native-reanimated+3.16.6+002+fix-onlayout.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +index 673ebd1..d5cb067 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +@@ -621,6 +621,11 @@ bool NativeReanimatedModule::handleRawEvent( + if (eventType.rfind("top", 0) == 0) { + eventType = "on" + eventType.substr(3); + } ++ ++ if (!isAnyHandlerWaitingForEvent(eventType, tag)) { ++ return false; ++ } ++ + jsi::Runtime &rt = uiWorkletRuntime_->getJSIRuntime(); + #if REACT_NATIVE_MINOR_VERSION >= 73 + const auto &eventPayload = rawEvent.eventPayload; diff --git a/patches/react-native-reanimated+3.16.6+003+fix-reanimated-commit-hook.patch b/patches/react-native-reanimated+3.16.6+003+fix-reanimated-commit-hook.patch new file mode 100644 index 0000000..27e6752 --- /dev/null +++ b/patches/react-native-reanimated+3.16.6+003+fix-reanimated-commit-hook.patch @@ -0,0 +1,174 @@ +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp +index 4ad8463..70ccf81 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ReanimatedCommitHook.cpp +@@ -78,7 +78,7 @@ RootShadowNode::Unshared ReanimatedCommitHook::shadowTreeWillCommit( + propsMap[&family].emplace_back(props); + }); + +- rootNode = cloneShadowTreeWithNewProps(*rootNode, propsMap); ++ rootNode = cloneShadowTreeWithNewPropsUnmounted(rootNode, propsMap); + + // If the commit comes from React Native then pause commits from + // Reanimated since the ShadowTree to be committed by Reanimated may not +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp +index 5795b73..02637a6 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.cpp +@@ -1,5 +1,6 @@ + #ifdef RCT_NEW_ARCH_ENABLED + ++#include + #include + + #include +@@ -7,6 +8,29 @@ + + namespace reanimated { + ++ChildrenMap calculateChildrenMap( ++ const RootShadowNode &oldRootNode, ++ const PropsMap &propsMap) { ++ ChildrenMap childrenMap; ++ ++ for (auto &[family, _] : propsMap) { ++ const auto ancestors = family->getAncestors(oldRootNode); ++ ++ for (const auto &[parentNode, index] : ++ std::ranges::reverse_view(ancestors)) { ++ const auto parentFamily = &parentNode.get().getFamily(); ++ auto &affectedChildren = childrenMap[parentFamily]; ++ ++ if (affectedChildren.contains(index)) { ++ continue; ++ } ++ ++ affectedChildren.insert(index); ++ } ++ } ++ return childrenMap; ++} ++ + ShadowNode::Unshared cloneShadowTreeWithNewPropsRecursive( + const ShadowNode &shadowNode, + const ChildrenMap &childrenMap, +@@ -43,33 +67,92 @@ ShadowNode::Unshared cloneShadowTreeWithNewPropsRecursive( + return result; + } + +-RootShadowNode::Unshared cloneShadowTreeWithNewProps( +- const RootShadowNode &oldRootNode, ++ShadowNode::Unshared cloneShadowTreeWithNewPropsUnmountedRecursive( ++ ShadowNode::Shared const &oldShadowNode, ++ const ChildrenMap &childrenMap, + const PropsMap &propsMap) { +- ChildrenMap childrenMap; ++ if (oldShadowNode->getHasBeenPromoted()) { ++ return cloneShadowTreeWithNewPropsRecursive( ++ *oldShadowNode, childrenMap, propsMap); ++ } + +- for (auto &[family, _] : propsMap) { +- const auto ancestors = family->getAncestors(oldRootNode); ++ auto shadowNode = std::const_pointer_cast(oldShadowNode); ++ auto layoutableShadowNode = ++ std::dynamic_pointer_cast(shadowNode); ++ if (layoutableShadowNode) { ++ layoutableShadowNode->dirtyLayout(); ++ } + +- for (const auto &[parentNode, index] : +- std::ranges::reverse_view(ancestors)) { +- const auto parentFamily = &parentNode.get().getFamily(); +- auto &affectedChildren = childrenMap[parentFamily]; ++ const auto family = &shadowNode->getFamily(); ++ const auto affectedChildrenIt = childrenMap.find(family); ++ const auto propsIt = propsMap.find(family); ++ auto children = shadowNode->getChildren(); + +- if (affectedChildren.contains(index)) { +- continue; ++ if (affectedChildrenIt != childrenMap.end()) { ++ for (const auto index : affectedChildrenIt->second) { ++ auto clone = cloneShadowTreeWithNewPropsUnmountedRecursive( ++ children[index], childrenMap, propsMap); ++ if (clone != children[index]) { ++ shadowNode->replaceChild(*children[index], clone, index); + } ++ } ++ } + +- affectedChildren.insert(index); ++ Props::Shared newProps = nullptr; ++ ++ if (propsIt != propsMap.end()) { ++ PropsParserContext propsParserContext{ ++ shadowNode->getSurfaceId(), *shadowNode->getContextContainer()}; ++ newProps = shadowNode->getProps(); ++ for (const auto &props : propsIt->second) { ++ newProps = shadowNode->getComponentDescriptor().cloneProps( ++ propsParserContext, newProps, RawProps(props)); + } + } + ++ if (newProps) { ++ auto &props = shadowNode->getProps(); ++ auto &mutableProps = const_cast(props); ++ ++#ifdef ANDROID ++ auto &newPropsRef = const_cast(*newProps); ++ newPropsRef.rawProps = mergeDynamicProps( ++ mutableProps->rawProps, ++ newProps->rawProps, ++ NullValueStrategy::Override); ++#endif ++ mutableProps = newProps; ++ auto layoutableShadowNode = ++ static_pointer_cast(shadowNode); ++ layoutableShadowNode->updateYogaProps(); ++ } ++ ++ return shadowNode; ++} ++ ++RootShadowNode::Unshared cloneShadowTreeWithNewProps( ++ const RootShadowNode &oldRootNode, ++ const PropsMap &propsMap) { ++ auto childrenMap = calculateChildrenMap(oldRootNode, propsMap); ++ + // This cast is safe, because this function returns a clone + // of the oldRootNode, which is an instance of RootShadowNode + return std::static_pointer_cast( + cloneShadowTreeWithNewPropsRecursive(oldRootNode, childrenMap, propsMap)); + } + ++RootShadowNode::Unshared cloneShadowTreeWithNewPropsUnmounted( ++ RootShadowNode::Unshared const &oldRootNode, ++ const PropsMap &propsMap) { ++ auto childrenMap = calculateChildrenMap(*oldRootNode, propsMap); ++ ++ // This cast is safe, because this function returns a clone ++ // of the oldRootNode, which is an instance of RootShadowNode ++ return std::static_pointer_cast( ++ cloneShadowTreeWithNewPropsUnmountedRecursive( ++ oldRootNode, childrenMap, propsMap)); ++} ++ + } // namespace reanimated + + #endif // RCT_NEW_ARCH_ENABLED +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h +index e3f9f6d..8240776 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/Fabric/ShadowTreeCloner.h +@@ -24,6 +24,10 @@ RootShadowNode::Unshared cloneShadowTreeWithNewProps( + const RootShadowNode &oldRootNode, + const PropsMap &propsMap); + ++RootShadowNode::Unshared cloneShadowTreeWithNewPropsUnmounted( ++ RootShadowNode::Unshared const &oldRootShadowNode, ++ const PropsMap &propsMap); ++ + } // namespace reanimated + + #endif // RCT_NEW_ARCH_ENABLED diff --git a/src/usecase/settings/ConnectionSettings.tsx b/src/usecase/settings/ConnectionSettings.tsx index a0648c6..8940cd5 100644 --- a/src/usecase/settings/ConnectionSettings.tsx +++ b/src/usecase/settings/ConnectionSettings.tsx @@ -16,7 +16,6 @@ import type { StoreState } from '../../store'; import { setConnectionInfoAction } from '../../store/actions'; import type { RootStackParamList } from '../Root'; import { styles } from './styles'; -import UndoTextInput from '../shared/UndoTextInput'; const connector = connect((state: StoreState) => ({ hostname: state.connection.hostname || '', @@ -111,7 +110,7 @@ class ConnectionSettings extends React.PureComponent { click the connect icon. Hostname will be prepended with the appropriate scheme (http(s)://) and suffixed with /weechat. - = ({ navigation }) => { from the camera roll. Press the button twice to upload media from elsewhere on your device. Press and hold the button to take a photo. - = ({ navigation }) => { {state.basicAuth && ( <> - = ({ navigation }) => { /> )} - = ({ navigation }) => { onChangeText={(fieldName) => setState({ fieldName })} value={state.fieldName} /> - = ({ navigation }) => { columnGap: 10 }} > - = ({ navigation }) => { value={headerName} onChangeText={(text) => setUploadOptionsHeaderName(index, text)} /> - ; - -const UndoTextInput: React.FC = ({ value, onChangeText, ...rest }) => { - const lastValue = useRef(); - const textInput = useRef(null); - - const handleChangeText = (textValue: string) => { - lastValue.current = textValue; - onChangeText?.(textValue); - }; - - useEffect(() => { - if (value !== lastValue.current) { - lastValue.current = value; - textInput.current?.setNativeProps({ text: value }); - } - }, [value]); - - return ( - - ); -}; - -export default UndoTextInput;