diff --git a/android/src/main/new_arch/MarkdownCommitHook.cpp b/android/src/main/new_arch/MarkdownCommitHook.cpp index bbaef3010..78b10e483 100644 --- a/android/src/main/new_arch/MarkdownCommitHook.cpp +++ b/android/src/main/new_arch/MarkdownCommitHook.cpp @@ -107,26 +107,31 @@ RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( rootNode = rootNode->cloneTree( nodes.textInput->getFamily(), [this, &stateData, &textInputState, &nodes](ShadowNode const &node) { - auto newStateData = - std::make_shared(stateData); - // force measurement of a map buffer - newStateData->cachedAttributedStringId = 0; - - // setting -1 as the event counter makes sure that the update will be ignored by the java - // part of the code, which is what we want as we don't change the attributed string here - if (previousEventCount_.contains(nodes.textInput->getTag()) && + std::shared_ptr newNode = nullptr; + + if (stateData.cachedAttributedStringId != 0) { + auto newStateData = + std::make_shared(stateData); + + // force measurement of a map buffer + newStateData->cachedAttributedStringId = 0; + + // setting -1 as the event counter makes sure that the update will be ignored by the java + // part of the code, which is what we want as we don't change the attributed string here + if (previousEventCount_.contains(nodes.textInput->getTag()) && previousEventCount_[nodes.textInput->getTag()] == stateData.mostRecentEventCount) { newStateData->mostRecentEventCount = -1; - } else { + } else { previousEventCount_[nodes.textInput->getTag()] = stateData.mostRecentEventCount; - } + } + + auto newState = std::make_shared>( + newStateData, textInputState); - // clone the text input with the new state - auto newNode = node.clone({ - .state = - std::make_shared>( - newStateData, textInputState), - }); + newNode = node.clone({ .state = newState }); + } else { + newNode = node.clone({}); + } const auto currentDecoratorProps = nodes.decorator->getProps()->rawProps["markdownStyle"]; diff --git a/apple/MarkdownCommitHook.h b/apple/MarkdownCommitHook.h index ac6cc45e2..55d1c02db 100644 --- a/apple/MarkdownCommitHook.h +++ b/apple/MarkdownCommitHook.h @@ -8,6 +8,7 @@ #include #include "MarkdownTextInputDecoratorShadowNode.h" +#include "RCTMarkdownUtils.h" using namespace facebook::react; @@ -35,6 +36,10 @@ class MarkdownCommitHook : public UIManagerCommitHook { RootShadowNode::Unshared const &newRootShadowNode) noexcept override; private: + static RCTMarkdownUtils * + getMarkdownUtils(const MarkdownTextInputDecoratorShadowNode &decorator); + static RCTMarkdownUtils *getOrCreateMarkdownUtils( + const MarkdownTextInputDecoratorShadowNode &decorator); const std::shared_ptr uiManager_; }; diff --git a/apple/MarkdownCommitHook.mm b/apple/MarkdownCommitHook.mm index f1923a3d5..85040b016 100644 --- a/apple/MarkdownCommitHook.mm +++ b/apple/MarkdownCommitHook.mm @@ -3,11 +3,11 @@ #include #include #include +#include #include "MarkdownCommitHook.h" #include "MarkdownShadowFamilyRegistry.h" #include "RCTMarkdownStyle.h" -#include "RCTMarkdownUtils.h" using namespace facebook::react; @@ -24,6 +24,27 @@ uiManager_->unregisterCommitHook(*this); } +RCTMarkdownUtils *MarkdownCommitHook::getMarkdownUtils( + const MarkdownTextInputDecoratorShadowNode &decorator) { + const auto decoratorState = std::static_pointer_cast< + const ConcreteState>( + decorator.getState()); + const auto memoizedUtils = (RCTMarkdownUtils *)unwrapManagedObject( + decoratorState->getData().markdownUtils); + return memoizedUtils; +} + +RCTMarkdownUtils *MarkdownCommitHook::getOrCreateMarkdownUtils( + const MarkdownTextInputDecoratorShadowNode &decorator) { + const auto memoizedUtils = MarkdownCommitHook::getMarkdownUtils(decorator); + + if (memoizedUtils != nullptr) { + return memoizedUtils; + } else { + return [[RCTMarkdownUtils alloc] init]; + } +} + RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit( ShadowTree const &, RootShadowNode::Shared const &, RootShadowNode::Unshared const &newRootShadowNode) noexcept { @@ -94,6 +115,8 @@ const auto fontSizeMultiplier = newRootShadowNode->getConcreteProps().layoutContext.fontSizeMultiplier; + RCTMarkdownUtils *usedUtils = nil; + // We only want to update the shadow node when the attributed string is // stored by value If it's stored by pointer, the markdown formatting should // already by applied to it, since the only source of that pointer (besides @@ -115,7 +138,7 @@ nodes.textInput->getTag())) { rootNode = rootNode->cloneTree( nodes.textInput->getFamily(), - [&nodes, &textInputState, &stateData, + [&nodes, &textInputState, &stateData, &usedUtils, fontSizeMultiplier](const ShadowNode &node) { const auto &markdownProps = *std::static_pointer_cast< MarkdownTextInputDecoratorViewProps const>( @@ -132,8 +155,9 @@ // this can possibly be optimized RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; - RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; - [utils setMarkdownStyle:markdownStyle]; + usedUtils = + MarkdownCommitHook::getOrCreateMarkdownUtils(*nodes.decorator); + [usedUtils setMarkdownStyle:markdownStyle]; // convert the attibuted string stored in state to // NSAttributedString @@ -142,14 +166,15 @@ stateData.attributedStringBox); // Handles the first render, where the text stored in props is - // different than the one stored in state. The one in state is empty, - // while the one in props is passed from JS. If we don't update the - // state here, we'll end up with a one-default-line-sized text input + // different than the one stored in state. The one in state is + // empty, while the one in props is passed from JS. If we don't + // update the state here, we'll end up with a one-default-line-sized + // text input if (textInputState.getRevision() == State::initialRevisionValue) { - auto plainStringFromState = - std::string([[nsAttributedString string] UTF8String]); + auto plainStringFromState = + std::string([[nsAttributedString string] UTF8String]); - if (plainStringFromState != textInputProps.text) { + if (plainStringFromState != textInputProps.text) { // creates new AttributedString from props, adapted from // TextInputShadowNode (ios one, text inputs are // platform-specific) @@ -164,14 +189,15 @@ // convert the newly created attributed string to // NSAttributedString - nsAttributedString = RCTNSAttributedStringFromAttributedStringBox( - AttributedStringBox{attributedString}); - } + nsAttributedString = + RCTNSAttributedStringFromAttributedStringBox( + AttributedStringBox{attributedString}); + } } // apply markdown - auto newString = [utils parseMarkdown:nsAttributedString - withAttributes:defaultNSTextAttributes]; + auto newString = [usedUtils parseMarkdown:nsAttributedString + withAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown @@ -187,10 +213,10 @@ }); }); } else if (stateData.attributedStringBox.getMode() == - AttributedStringBox::Mode::OpaquePointer) { + AttributedStringBox::Mode::OpaquePointer) { rootNode = rootNode->cloneTree( nodes.textInput->getFamily(), - [&nodes, &textInputState, &stateData, + [&nodes, &textInputState, &stateData, &usedUtils, fontSizeMultiplier](const ShadowNode &node) { const auto &markdownProps = *std::static_pointer_cast< MarkdownTextInputDecoratorViewProps const>( @@ -207,8 +233,9 @@ // this can possibly be optimized RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc] initWithStruct:markdownProps.markdownStyle]; - RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init]; - [utils setMarkdownStyle:markdownStyle]; + usedUtils = + MarkdownCommitHook::getOrCreateMarkdownUtils(*nodes.decorator); + [usedUtils setMarkdownStyle:markdownStyle]; // convert the attibuted string stored in state to // NSAttributedString @@ -217,8 +244,8 @@ stateData.attributedStringBox); // apply markdown - auto newString = [utils parseMarkdown:nsAttributedString - withAttributes:defaultNSTextAttributes]; + auto newString = [usedUtils parseMarkdown:nsAttributedString + withAttributes:defaultNSTextAttributes]; // create a clone of the old TextInputState and update the // attributed string box to point to the string with markdown @@ -234,6 +261,39 @@ }); }); } + + // if usedUtils is not nil, then we did some work on the TextInput + // ShadowNode, we may need to update the decorator as well + if (usedUtils != nil) { + const auto ancestors = + nodes.decorator->getFamily().getAncestors(*rootNode); + const auto parentInfo = ancestors.back(); + const auto decoratorNode = + std::static_pointer_cast( + parentInfo.first.get().getChildren().at(parentInfo.second)); + + // if usedUtils is defferent from the one kept in the decorator state, it + // needs to be updated to ensure memoization works correctly + if (usedUtils != MarkdownCommitHook::getMarkdownUtils(*decoratorNode)) { + const auto oldDecoratorState = *std::static_pointer_cast< + const ConcreteState>( + decoratorNode->getState()); + const auto newDecoratorState = + std::make_shared( + oldDecoratorState.getData().decoratorFamily, + wrapManagedObject(usedUtils)); + const auto newDecoratorNode = decoratorNode->clone( + {.state = std::make_shared< + const ConcreteState>( + newDecoratorState, oldDecoratorState)}); + + // since we did clone the path to the text input, parent node is mutable + // at this point - no need to clone the entire path + const auto mutableParent = + const_cast(&parentInfo.first.get()); + mutableParent->replaceChild(*decoratorNode, newDecoratorNode); + } + } } return std::static_pointer_cast(rootNode); diff --git a/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h index 7590b9968..d931f734d 100644 --- a/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h +++ b/cpp/react/renderer/components/RNLiveMarkdownSpec/MarkdownTextInputDecoratorState.h @@ -1,5 +1,6 @@ #pragma once +#include #include namespace facebook { @@ -9,18 +10,25 @@ class JSI_EXPORT MarkdownTextInputDecoratorState final { public: using Shared = std::shared_ptr; - MarkdownTextInputDecoratorState() : decoratorFamily(nullptr){}; + MarkdownTextInputDecoratorState() + : decoratorFamily(nullptr), markdownUtils(nullptr){}; MarkdownTextInputDecoratorState( const ShadowNodeFamily::Shared textInputFamily_) - : decoratorFamily(textInputFamily_){}; + : decoratorFamily(textInputFamily_), markdownUtils(nullptr){}; + MarkdownTextInputDecoratorState( + const ShadowNodeFamily::Shared textInputFamily_, + const std::shared_ptr markdownUtils_) + : decoratorFamily(textInputFamily_), markdownUtils(markdownUtils_){}; #ifdef ANDROID MarkdownTextInputDecoratorState( MarkdownTextInputDecoratorState const &previousState, folly::dynamic data) - : decoratorFamily(previousState.decoratorFamily){}; + : decoratorFamily(previousState.decoratorFamily), + markdownUtils(nullptr){}; #endif const ShadowNodeFamily::Shared decoratorFamily; + const std::shared_ptr markdownUtils; #ifdef ANDROID folly::dynamic getDynamic() const { diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index e42ec5bc5..6003a05a4 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1497,7 +1497,7 @@ PODS: - React-logger (= 0.75.2) - React-perflogger (= 0.75.2) - React-utils (= 0.75.2) - - RNLiveMarkdown (0.1.183): + - RNLiveMarkdown (0.1.184): - DoubleConversion - glog - hermes-engine @@ -1517,9 +1517,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNLiveMarkdown/newarch (= 0.1.183) + - RNLiveMarkdown/newarch (= 0.1.184) - Yoga - - RNLiveMarkdown/newarch (0.1.183): + - RNLiveMarkdown/newarch (0.1.184): - DoubleConversion - glog - hermes-engine @@ -1805,7 +1805,7 @@ SPEC CHECKSUMS: React-utils: 81a715d9c0a2a49047e77a86f3a2247408540deb ReactCodegen: 4eedb2fdd079174d6fc3c80b1cccafbe4ff1be8d ReactCommon: 6ef348087d250257c44c0204461c03f036650e9b - RNLiveMarkdown: fa9c6451960d09209bb5698745a0a66330ec53cc + RNLiveMarkdown: d5a311813de4429b9f18e764badc5bd7923cbaea SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d Yoga: 2a45d7e59592db061217551fd3bbe2dd993817ae diff --git a/package.json b/package.json index 7569b90ba..32fbb0461 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@expensify/react-native-live-markdown", - "version": "0.1.184", + "version": "0.1.186", "description": "Drop-in replacement for React Native's TextInput component with Markdown formatting.", "main": "lib/commonjs/index", "module": "lib/module/index",