├── .watchmanconfig ├── example ├── .node-version ├── .watchmanconfig ├── .bundle │ └── config ├── app.json ├── Gemfile ├── .eslintrc.js ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── AndroidManifest.xml │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── wishlistexample │ │ │ │ │ ├── MainActivity.java │ │ │ │ │ └── MainApplication.java │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── release │ │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── wishlistexample │ │ │ │ └── ReactNativeFlipper.java │ │ └── proguard-rules.pro │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ └── gradlew.bat ├── src │ ├── AssetList │ │ ├── assets │ │ │ ├── eth.png │ │ │ ├── arbitrumBadge.png │ │ │ ├── ethereumBadge.png │ │ │ ├── optimismBadge.png │ │ │ ├── polygonBadge.png │ │ │ └── polygonBadgeDark.png │ │ ├── Header.tsx │ │ ├── AssetListHeader.tsx │ │ ├── Button.tsx │ │ ├── ItemCheckbox.tsx │ │ ├── AssetIcon.tsx │ │ ├── assets.ts │ │ ├── AssetListSeparator.tsx │ │ ├── AssetListSpec.md │ │ └── AssetItem.tsx │ ├── Chat │ │ ├── assets │ │ │ ├── refresh.png │ │ │ ├── add_reaction.png │ │ │ └── margelo_logo.png │ │ ├── LoadingView.tsx │ │ ├── ReactionPicker.tsx │ │ ├── ChatList.tsx │ │ ├── MessageInput.tsx │ │ ├── ChatHeader.tsx │ │ └── Data.ts │ └── App.tsx ├── ios │ ├── WishlistExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── AppDelegate.h │ │ ├── main.m │ │ ├── AppDelegate.mm │ │ └── Info.plist │ ├── WishlistExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── .xcode.env │ ├── WishlistExampleTests │ │ ├── Info.plist │ │ └── WishlistExampleTests.m │ └── Podfile ├── index.js ├── react-native.config.js ├── babel.config.js ├── package.json ├── metro.config.js └── Gemfile.lock ├── .eslintignore ├── src ├── global.d.ts ├── __tests__ │ ├── WistList-test.tsx │ └── WishlistData-test.tsx ├── Components │ ├── WishlistView.tsx │ ├── ForEachBase.tsx │ ├── WishlistText.tsx │ ├── IF.tsx │ ├── ForEach.tsx │ ├── WishlistImage.tsx │ ├── Switch.tsx │ └── Pressable.tsx ├── Specs │ ├── NativeWishlistManager.ts │ ├── NativeTemplateInterceptor.ts │ ├── NativeContentContainer.ts │ ├── NativeTemplateContainer.ts │ └── NativeWishlist.ts ├── Utils.ts ├── TemplateContext.tsx ├── WishlistContext.tsx ├── OrchestratorBinding.ts ├── TemplateItem.ts ├── renderTemplate.ts ├── index.ts ├── WishlistJsRuntime.ts ├── ComponentPool.ts ├── EventHandler.ts └── TemplateValue.tsx ├── cpp ├── WishlistDefine.h ├── UIScheduler │ ├── MGUIScheduler.cpp │ └── MGUIScheduler.hpp ├── MGErrorHandler.h ├── MGViewportCarer │ ├── MGVSyncRequester.hpp │ ├── MGViewportCarerListener.hpp │ ├── MGViewportCarer.hpp │ └── MGViewportCarerImpl.h ├── Wishlist │ ├── MGWishlistComponentDescriptor.h │ ├── MGWishlistState.h │ ├── MGWishlistShadowNode.h │ ├── MGWishlistState.cpp │ └── MGWishlistShadowNode.cpp ├── DataBinding │ ├── MGBindingProvider.hpp │ ├── MGDataBinding.hpp │ └── MGDataBindingImpl.hpp ├── TemplateContainer │ ├── MGTemplateContainerComponentDescriptor.h │ ├── MGTemplateContainerState.cpp │ ├── MGTemplateContainerState.h │ ├── MGTemplateContainerShadowNode.h │ └── MGTemplateContainerShadowNode.cpp ├── DependencyInjection │ ├── MGUIManagerHolder.cpp │ ├── MGUIManagerHolder.h │ ├── MGDI.hpp │ ├── MGDIImpl.hpp │ └── MGDIImpl.cpp ├── ContentContainer │ ├── MGContentContainerShadowNode.cpp │ ├── MGContentContainerState.cpp │ ├── MGContentContainerShadowNode.h │ ├── MGContentContainerState.h │ └── MGContentContainerComponentDescriptor.h ├── WishlistJsRuntime.h ├── ItemProvider │ ├── ComponentsPool.h │ ├── ShadowNodeCopyMachine.h │ ├── ItemProvider.h │ ├── ShadowNodeBinding.h │ ├── ItemProvider.cpp │ ├── ShadowNodeCopyMachine.cpp │ └── ComponentsPool.cpp └── WishlistJsRuntime.cpp ├── .gitattributes ├── tsconfig.build.json ├── babel.config.js ├── .yarnrc ├── android ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── jni │ │ ├── ErrorHandlerAndroid.h │ │ ├── OnLoad.cpp │ │ ├── ErrorHandlerAndroid.cpp │ │ ├── UISchedulerAndroid.h │ │ ├── JNIStateRegistry.cpp │ │ ├── JNIStateRegistry.h │ │ ├── wishlist.h │ │ ├── UISchedulerAndroid.cpp │ │ ├── WishlistManagerModule.hpp │ │ ├── Orchestrator.hpp │ │ └── WishlistManagerModule.cpp │ │ └── java │ │ └── com │ │ └── wishlist │ │ ├── UIScheduler.kt │ │ ├── WishlistSoLoader.kt │ │ ├── WishlistPackage.kt │ │ ├── TemplateInterceptor.kt │ │ ├── ContentContainer.kt │ │ ├── TemplateInterceptorViewManager.kt │ │ ├── TemplateContainer.kt │ │ ├── Orchestrator.kt │ │ ├── WishlistManagerModule.kt │ │ ├── TemplateContainerViewManager.kt │ │ └── WishlistViewManager.kt ├── gradle.properties ├── spotless.gradle ├── build.gradle ├── CMakeLists.txt └── gradlew.bat ├── .eslintrc.js ├── ios ├── MGContentContainerComponent.h ├── MGTemplateInterceptorComponent.h ├── MGErrorHandlerIOS.mm ├── MGWishlistQueue.h ├── MGErrorHandlerIOS.h ├── MGTemplateContainerComponent.h ├── UIScheduleriOS │ ├── MGUIScheduleriOS.hpp │ └── MGUIScheduleriOS.mm ├── MGWishlistManager.h ├── MGWishListComponent.h ├── Orchestrator │ ├── MGOrchestratorCppAdapter.cpp │ ├── MGOrchestratorCppAdapter.hpp │ └── MGOrchestrator.h ├── MGWishlistQueue.m ├── MGContentContainerComponent.mm ├── MGObjCJSIUtils.h ├── MGTemplateContainerComponent.mm └── MGTemplateInterceptorComponent.mm ├── .editorconfig ├── react-native.config.js ├── tsconfig.json ├── scripts └── bootstrap.js ├── .gitignore ├── MGWishList.podspec ├── lefthook.yml ├── LICENSE ├── README.md ├── .github └── workflows │ ├── build-android.yml │ ├── validate-js.yml │ └── build-ios.yml ├── .vscode └── settings.json └── .clang-format /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /example/.node-version: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare var global: any; 2 | -------------------------------------------------------------------------------- /cpp/WishlistDefine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MG_WISHLIST_DEBUG 0 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WishlistExample", 3 | "displayName": "WishlistExample" 4 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": "./tsconfig", 4 | "exclude": ["example"] 5 | } 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby ">= 2.6.10" 4 | 5 | gem 'cocoapods', '~> 1.12' 6 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'react-native/no-inline-styles': 0, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /example/src/AssetList/assets/eth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/eth.png -------------------------------------------------------------------------------- /example/src/Chat/assets/refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/Chat/assets/refresh.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | WishlistExample 3 | 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/WishlistExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/src/Chat/assets/add_reaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/Chat/assets/add_reaction.png -------------------------------------------------------------------------------- /example/src/Chat/assets/margelo_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/Chat/assets/margelo_logo.png -------------------------------------------------------------------------------- /example/src/AssetList/assets/arbitrumBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/arbitrumBadge.png -------------------------------------------------------------------------------- /example/src/AssetList/assets/ethereumBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/ethereumBadge.png -------------------------------------------------------------------------------- /example/src/AssetList/assets/optimismBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/optimismBadge.png -------------------------------------------------------------------------------- /example/src/AssetList/assets/polygonBadge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/polygonBadge.png -------------------------------------------------------------------------------- /src/__tests__/WistList-test.tsx: -------------------------------------------------------------------------------- 1 | describe('WishList', () => { 2 | it('should write tests', () => { 3 | expect(true).toBe(true); 4 | }); 5 | }); 6 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/ios/WishlistExample/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : RCTAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/src/AssetList/assets/polygonBadgeDark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/src/AssetList/assets/polygonBadgeDark.png -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@react-native-community', 'prettier'], 4 | rules: { 5 | 'prettier/prettier': 'error', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /cpp/UIScheduler/MGUIScheduler.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGUIScheduler.cpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #include "MGUIScheduler.hpp" 9 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | Wishlist_kotlinVersion=1.6.21 2 | Wishlist_minSdkVersion=21 3 | Wishlist_targetSdkVersion=33 4 | Wishlist_compileSdkVersion=33 5 | Wishlist_ndkVersion=23.1.7779620 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/margelo/react-native-wishlist/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import { name as appName } from './app.json'; 3 | import { App } from './src/App'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | dependencies: { 5 | wishlist: { 6 | root: path.join(__dirname, '..'), 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /src/Components/WishlistView.tsx: -------------------------------------------------------------------------------- 1 | import { View } from 'react-native'; 2 | import { createTemplateComponent } from '../createTemplateComponent'; 3 | 4 | export const WishlistView = createTemplateComponent(View); 5 | -------------------------------------------------------------------------------- /ios/MGContentContainerComponent.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface MGContentContainerComponent : RCTViewComponentView 6 | 7 | @end 8 | 9 | NS_ASSUME_NONNULL_END 10 | -------------------------------------------------------------------------------- /cpp/MGErrorHandler.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Wishlist { 6 | 7 | class MGErrorHandler { 8 | public: 9 | virtual void reportError(const std::string &message) = 0; 10 | }; 11 | 12 | }; // namespace Wishlist 13 | -------------------------------------------------------------------------------- /ios/MGTemplateInterceptorComponent.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | @interface MGTemplateInterceptorComponent : RCTViewComponentView 6 | 7 | @end 8 | 9 | NS_ASSUME_NONNULL_END 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/Specs/NativeWishlistManager.ts: -------------------------------------------------------------------------------- 1 | import { TurboModuleRegistry, TurboModule } from 'react-native'; 2 | 3 | export interface Spec extends TurboModule { 4 | install(): boolean; 5 | } 6 | 7 | export default TurboModuleRegistry.getEnforcing('WishlistManager'); 8 | -------------------------------------------------------------------------------- /example/ios/WishlistExample/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | @autoreleasepool { 8 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /ios/MGErrorHandlerIOS.mm: -------------------------------------------------------------------------------- 1 | #import "MGErrorHandlerIOS.h" 2 | 3 | #import 4 | 5 | namespace Wishlist { 6 | 7 | void MGErrorHandlerIOS::reportError(const std::string &message) 8 | { 9 | RCTLogError(@"%@", @(message.c_str())); 10 | } 11 | 12 | }; // namespace Wishlist 13 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'WishlistExample' 2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) 3 | include ':app' 4 | includeBuild('../node_modules/@react-native/gradle-plugin') 5 | -------------------------------------------------------------------------------- /src/Components/ForEachBase.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { View } from 'react-native'; 3 | 4 | // This needs to be split to avoid circular dependency. 5 | 6 | export const ForEachBase = forwardRef((props, ref) => { 7 | return ; 8 | }); 9 | -------------------------------------------------------------------------------- /ios/MGWishlistQueue.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | NS_ASSUME_NONNULL_BEGIN 4 | 5 | RCT_EXTERN dispatch_queue_t MGGetWishlistQueue(void); 6 | 7 | RCT_EXTERN BOOL MGIsWishlistQueue(void); 8 | 9 | RCT_EXTERN void MGExecuteOnWishlistQueue(dispatch_block_t block); 10 | 11 | NS_ASSUME_NONNULL_END 12 | -------------------------------------------------------------------------------- /ios/MGErrorHandlerIOS.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "MGErrorHandler.h" 5 | 6 | namespace Wishlist { 7 | 8 | class MGErrorHandlerIOS final : public MGErrorHandler { 9 | public: 10 | void reportError(const std::string &message) override; 11 | }; 12 | 13 | } // namespace Wishlist 14 | -------------------------------------------------------------------------------- /android/src/main/jni/ErrorHandlerAndroid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "MGErrorHandler.h" 5 | 6 | namespace Wishlist { 7 | 8 | class ErrorHandlerAndroid final : public MGErrorHandler { 9 | public: 10 | void reportError(const std::string &message) override; 11 | }; 12 | 13 | } // namespace Wishlist 14 | -------------------------------------------------------------------------------- /example/ios/WishlistExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/WishlistExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/MGTemplateContainerComponent.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "MGWishListComponent.h" 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @interface MGTemplateContainerComponent : RCTViewComponentView 7 | 8 | - (void)setWishlist:(MGWishListComponent *)wishList; 9 | 10 | @end 11 | 12 | NS_ASSUME_NONNULL_END 13 | -------------------------------------------------------------------------------- /cpp/MGViewportCarer/MGVSyncRequester.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGVSyncRequester.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | 12 | namespace Wishlist { 13 | 14 | struct MGVSyncRequester { 15 | virtual void requestVSync() = 0; 16 | }; 17 | 18 | }; // namespace Wishlist 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/Specs/NativeTemplateInterceptor.ts: -------------------------------------------------------------------------------- 1 | import type { ViewProps } from 'react-native'; 2 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; 3 | 4 | export interface NativeTemplateInterceptorProps extends ViewProps {} 5 | 6 | export default codegenNativeComponent( 7 | 'MGTemplateInterceptor', 8 | ); 9 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | let idGenerator = 0; 4 | 5 | export function generateId(): string { 6 | return `id_${idGenerator++}`; 7 | } 8 | 9 | export function useGeneratedId(): string { 10 | const ref = useRef(null); 11 | if (ref.current === null) { 12 | ref.current = generateId(); 13 | } 14 | return ref.current; 15 | } 16 | -------------------------------------------------------------------------------- /android/src/main/jni/OnLoad.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "Orchestrator.hpp" 3 | #include "WishlistManagerModule.hpp" 4 | 5 | using namespace Wishlist; 6 | 7 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { 8 | return facebook::jni::initialize(vm, [] { 9 | WishlistManagerModule::registerNatives(); 10 | Orchestrator::registerNatives(); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /cpp/Wishlist/MGWishlistComponentDescriptor.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | #include "MGWishlistShadowNode.h" 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | using MGWishlistComponentDescriptor = 11 | ConcreteComponentDescriptor; 12 | 13 | } // namespace react 14 | } // namespace facebook 15 | -------------------------------------------------------------------------------- /src/Specs/NativeContentContainer.ts: -------------------------------------------------------------------------------- 1 | import type { ViewProps } from 'react-native'; 2 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; 3 | 4 | export interface NativeContentContainerProps extends ViewProps {} 5 | 6 | export default codegenNativeComponent( 7 | 'MGContentContainer', 8 | { interfaceOnly: true }, 9 | ); 10 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GestureHandlerRootView } from 'react-native-gesture-handler'; 3 | import Chat from './Chat/ChatExample'; 4 | // import { AssetListExample } from './AssetList/AssetListExample'; 5 | 6 | export function App() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /cpp/UIScheduler/MGUIScheduler.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGUIScheduler.hpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #ifndef MGUIScheduler_hpp 9 | #define MGUIScheduler_hpp 10 | 11 | #include 12 | #include 13 | 14 | struct MGUIScheduler { 15 | virtual void scheduleOnUI(std::function &&f) = 0; 16 | }; 17 | 18 | #endif /* MGUIScheduler_hpp */ 19 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/UIScheduler.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.proguard.annotations.DoNotStrip 4 | import com.facebook.react.bridge.UiThreadUtil 5 | 6 | @DoNotStrip 7 | class UIScheduler { 8 | companion object { 9 | @DoNotStrip 10 | @JvmStatic 11 | fun scheduleOnUI(runnable: Runnable?) { 12 | UiThreadUtil.runOnUiThread(runnable) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/WishlistSoLoader.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.soloader.SoLoader 4 | 5 | class WishlistSoLoader { 6 | companion object { 7 | @Volatile private var didInit = false 8 | 9 | fun staticInit() { 10 | if (didInit) { 11 | return 12 | } 13 | SoLoader.loadLibrary("react_codegen_wishlist") 14 | didInit = true 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ios/UIScheduleriOS/MGUIScheduleriOS.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGUIScheduleriOS.hpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include "MGUIScheduler.hpp" 12 | 13 | namespace Wishlist { 14 | 15 | struct MGUIScheduleriOS : MGUIScheduler { 16 | virtual void scheduleOnUI(std::function &&f); 17 | }; 18 | 19 | }; // namespace Wishlist 20 | -------------------------------------------------------------------------------- /cpp/DataBinding/MGBindingProvider.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGBindingProvider.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace Wishlist { 14 | 15 | using namespace facebook; 16 | 17 | struct MGBindingProvider { 18 | virtual jsi::Value getBinding(jsi::Runtime &rt) = 0; 19 | }; 20 | 21 | }; // namespace Wishlist 22 | -------------------------------------------------------------------------------- /src/TemplateContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const TemplateContext = createContext<{ 4 | templateType: string; 5 | renderChildren?: boolean; 6 | } | null>(null); 7 | 8 | export function useTemplateContext() { 9 | const context = useContext(TemplateContext); 10 | if (!context) { 11 | throw Error('Must be rendered inside a Template component.'); 12 | } 13 | return context; 14 | } 15 | -------------------------------------------------------------------------------- /src/WishlistContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const WishlistContext = createContext<{ 4 | id: string; 5 | inflatorId: string; 6 | data: Object; 7 | } | null>(null); 8 | 9 | export function useWishlistContext() { 10 | const context = useContext(WishlistContext); 11 | if (!context) { 12 | throw Error('Must be rendered inside a Template component.'); 13 | } 14 | return context; 15 | } 16 | -------------------------------------------------------------------------------- /example/src/Chat/LoadingView.tsx: -------------------------------------------------------------------------------- 1 | import { View, ActivityIndicator, StyleSheet } from 'react-native'; 2 | import React from 'react'; 3 | 4 | export function LoadingView() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | const styles = StyleSheet.create({ 13 | container: { 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | padding: 16, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependency: { 3 | platforms: { 4 | android: { 5 | componentDescriptors: [ 6 | 'MGContentContainerComponentDescriptor', 7 | 'MGTemplateContainerComponentDescriptor', 8 | 'MGTemplateInterceptorComponentDescriptor', 9 | 'MGWishlistComponentDescriptor', 10 | ], 11 | cmakeListsPath: '../android/CMakeLists.txt', 12 | }, 13 | }, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /src/OrchestratorBinding.ts: -------------------------------------------------------------------------------- 1 | interface VisibleItem { 2 | index: number; 3 | key: string; 4 | } 5 | export interface ViewportObserver { 6 | markItemsDirty: (indices: Array) => void; 7 | markAllItemsDirty: () => void; 8 | getAllVisibleItems: () => Array; 9 | updateIndices: (newIndex: number) => void; 10 | } 11 | 12 | export function scheduleSyncUp(wishlistId: string) { 13 | 'worklet'; 14 | global.wishlists[wishlistId].scheduleSyncUp(); 15 | } 16 | -------------------------------------------------------------------------------- /src/TemplateItem.ts: -------------------------------------------------------------------------------- 1 | export type TemplateItem = { 2 | [key: string]: TemplateItem | undefined; 3 | } & { 4 | key: string; 5 | type: string; 6 | getByWishId: (id: string) => TemplateItem | undefined; 7 | addProps: (props: any) => void; 8 | setCallback: ( 9 | eventName: string, 10 | callback: (nativeEvent: any) => void, 11 | ) => void; 12 | describe: () => string; 13 | setChildren: (children: TemplateItem[]) => void; 14 | getTag: () => number; 15 | }; 16 | -------------------------------------------------------------------------------- /ios/UIScheduleriOS/MGUIScheduleriOS.mm: -------------------------------------------------------------------------------- 1 | // 2 | // MGUIScheduleriOS.cpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #import "MGUIScheduleriOS.hpp" 9 | #import 10 | 11 | namespace Wishlist { 12 | 13 | void MGUIScheduleriOS::scheduleOnUI(std::function &&f) 14 | { 15 | __block auto retainedWork = std::move(f); 16 | RCTExecuteOnMainQueue(^{ 17 | retainedWork(); 18 | }); 19 | }; 20 | 21 | }; // namespace Wishlist 22 | -------------------------------------------------------------------------------- /src/Specs/NativeTemplateContainer.ts: -------------------------------------------------------------------------------- 1 | import type { ViewProps } from 'react-native'; 2 | import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; 3 | 4 | export interface NativeTemplateContainerProps extends ViewProps { 5 | inflatorId: string; 6 | wishlistId: string; 7 | names: ReadonlyArray; 8 | } 9 | 10 | export default codegenNativeComponent( 11 | 'MGTemplateContainer', 12 | { interfaceOnly: true }, 13 | ); 14 | -------------------------------------------------------------------------------- /android/spotless.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.diffplug.spotless' 2 | 3 | allprojects { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | spotless { 11 | java { 12 | target 'src/**/*.java' 13 | googleJavaFormat() 14 | } 15 | kotlin { 16 | target 'src/**/*.kt' 17 | ktfmt() 18 | } 19 | groovyGradle { 20 | target '*.gradle' 21 | greclipse() 22 | indentWithSpaces(4) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cpp/DataBinding/MGDataBinding.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGDataBinding.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Wishlist { 16 | 17 | struct MGDataBinding { 18 | virtual std::set applyChangesAndGetDirtyIndices( 19 | std::pair windowIndexRange) = 0; 20 | virtual ~MGDataBinding() {} 21 | }; 22 | 23 | }; // namespace Wishlist 24 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /src/Components/WishlistText.tsx: -------------------------------------------------------------------------------- 1 | import { Text } from 'react-native'; 2 | import { createTemplateComponent } from '../createTemplateComponent'; 3 | 4 | export const WishlistText = createTemplateComponent(Text, { 5 | addProps: (item, props) => { 6 | 'worklet'; 7 | 8 | const { children, ...other } = props; 9 | const value = 10 | typeof children === 'string' ? children : children?.toString(); 11 | item.RawText?.addProps({ text: value }); 12 | item.addProps(other); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /android/src/main/jni/ErrorHandlerAndroid.cpp: -------------------------------------------------------------------------------- 1 | #include "ErrorHandlerAndroid.h" 2 | 3 | #include 4 | #include 5 | 6 | namespace Wishlist { 7 | 8 | using namespace facebook; 9 | 10 | void ErrorHandlerAndroid::reportError(const std::string &message) { 11 | // TODO: Our thread should have access to JVM by default. 12 | jni::ThreadScope::WithClassLoader( 13 | [&] { react::JReactCxxErrorHandler::handleError(message); }); 14 | } 15 | 16 | }; // namespace Wishlist 17 | -------------------------------------------------------------------------------- /cpp/TemplateContainer/MGTemplateContainerComponentDescriptor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "MGTemplateContainerShadowNode.h" 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | using MGTemplateContainerComponentDescriptor = 12 | ConcreteComponentDescriptor; 13 | 14 | } // namespace react 15 | } // namespace facebook 16 | -------------------------------------------------------------------------------- /android/src/main/jni/UISchedulerAndroid.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "MGUIScheduler.hpp" 5 | 6 | namespace Wishlist { 7 | 8 | using namespace facebook::jni; 9 | 10 | class UISchedulerAndroid final : public JavaClass, 11 | public MGUIScheduler { 12 | public: 13 | static auto constexpr kJavaDescriptor = "Lcom/wishlist/UIScheduler;"; 14 | 15 | void scheduleOnUI(std::function &&f) override; 16 | }; 17 | 18 | }; // namespace Wishlist 19 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /ios/MGWishlistManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | NS_ASSUME_NONNULL_BEGIN 5 | 6 | @interface MGWishlistManager : NSObject 7 | @end 8 | 9 | @interface MGWishlistComponentManager : RCTViewManager 10 | @end 11 | 12 | @interface MGTemplateContainerManager : RCTViewManager 13 | @end 14 | 15 | @interface MGTemplateInterceptorManager : RCTViewManager 16 | @end 17 | 18 | @interface MGContentContainerManager : RCTViewManager 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /src/renderTemplate.ts: -------------------------------------------------------------------------------- 1 | import { ComponentPool } from './ComponentPool'; 2 | import { getUIInflatorRegistry } from './InflatorRepository'; 3 | 4 | export function renderTemplate( 5 | template: string, 6 | value: any, 7 | rootValue: any, 8 | inflatorId: string, 9 | pool: ComponentPool, 10 | ) { 11 | 'worklet'; 12 | 13 | const item = pool.getComponent(template)!; 14 | return getUIInflatorRegistry().useMappings( 15 | item, 16 | value, 17 | template, 18 | inflatorId, 19 | pool, 20 | rootValue, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/jni/JNIStateRegistry.cpp: -------------------------------------------------------------------------------- 1 | #include "JNIStateRegistry.h" 2 | 3 | namespace Wishlist { 4 | 5 | JNIStateRegistry &JNIStateRegistry::getInstance() { 6 | static JNIStateRegistry instance; 7 | return instance; 8 | } 9 | 10 | JNIStateRegistry::JNIStateRegistry() : idGenerator_(0) {} 11 | 12 | void *JNIStateRegistry::getValue(int id) { 13 | return values_[id]; 14 | } 15 | 16 | int JNIStateRegistry::addValue(void *value) { 17 | int id = idGenerator_++; 18 | values_[id] = value; 19 | return id; 20 | } 21 | 22 | }; // namespace Wishlist 23 | -------------------------------------------------------------------------------- /cpp/MGViewportCarer/MGViewportCarerListener.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGViewportCarerListener.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include "MGViewportCarer.hpp" 13 | 14 | namespace Wishlist { 15 | 16 | struct Item { 17 | float offset; 18 | float height; 19 | int index; 20 | std::string key; 21 | }; 22 | 23 | struct MGViewportCarerListener { 24 | virtual void didPushChildren(std::vector newWindow) = 0; 25 | }; 26 | 27 | }; // namespace Wishlist 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import WishListManager from './Specs/NativeWishlistManager'; 2 | 3 | WishListManager.install(); 4 | 5 | export { useTemplateValue, TemplateValue } from './TemplateValue'; 6 | export { createTemplateComponent } from './createTemplateComponent'; 7 | export { Wishlist, WishListInstance } from './Wishlist'; 8 | export { 9 | useWishlistData, 10 | useWishlistContextData, 11 | WishlistData, 12 | } from './WishlistData'; 13 | export { createRunInJsFn, createRunInWishlistFn } from './WishlistJsRuntime'; 14 | export { renderTemplate } from './renderTemplate'; 15 | -------------------------------------------------------------------------------- /android/src/main/jni/JNIStateRegistry.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace Wishlist { 6 | 7 | class JNIStateRegistry { 8 | public: 9 | static JNIStateRegistry &getInstance(); 10 | 11 | void *getValue(int id); 12 | int addValue(void *value); 13 | 14 | private: 15 | JNIStateRegistry(); 16 | JNIStateRegistry(const JNIStateRegistry &) = delete; 17 | JNIStateRegistry &operator=(const JNIStateRegistry &) = delete; 18 | 19 | std::unordered_map values_; 20 | int idGenerator_; 21 | }; 22 | 23 | } // namespace Wishlist 24 | -------------------------------------------------------------------------------- /src/Components/IF.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View } from 'react-native'; 3 | import { createTemplateComponent } from '../createTemplateComponent'; 4 | 5 | const IFTemplateComponent = createTemplateComponent(View, { 6 | addProps: (item, props) => { 7 | 'worklet'; 8 | 9 | if (props.condition) { 10 | item.addProps({ display: 'flex' }); 11 | } else { 12 | item.addProps({ display: 'none' }); 13 | } 14 | }, 15 | }); 16 | 17 | // TODO(terry): Fix IF props type 18 | export function IF(props: any) { 19 | return ; 20 | } 21 | -------------------------------------------------------------------------------- /android/src/main/jni/wishlist.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "MGContentContainerComponentDescriptor.h" 7 | #include "MGTemplateContainerComponentDescriptor.h" 8 | #include "MGWishlistComponentDescriptor.h" 9 | 10 | namespace facebook { 11 | namespace react { 12 | 13 | JSI_EXPORT 14 | std::shared_ptr wishlist_ModuleProvider( 15 | const std::string &moduleName, 16 | const JavaTurboModule::InitParams ¶ms); 17 | 18 | } // namespace react 19 | } // namespace facebook 20 | -------------------------------------------------------------------------------- /cpp/DependencyInjection/MGUIManagerHolder.cpp: -------------------------------------------------------------------------------- 1 | #include "MGUIManagerHolder.h" 2 | 3 | namespace Wishlist { 4 | 5 | MGUIManagerHolder &MGUIManagerHolder::getInstance() { 6 | static MGUIManagerHolder instance; 7 | return instance; 8 | } 9 | 10 | MGUIManagerHolder::MGUIManagerHolder() : uiManager_(nullptr) {} 11 | 12 | std::shared_ptr MGUIManagerHolder::getUIManager() const { 13 | return uiManager_; 14 | } 15 | 16 | void MGUIManagerHolder::setUIManager( 17 | const std::shared_ptr &uiManager) { 18 | uiManager_ = uiManager; 19 | } 20 | 21 | }; // namespace Wishlist 22 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/WishlistJsRuntime.ts: -------------------------------------------------------------------------------- 1 | import { ContextType, IWorkletContext, Worklets } from 'react-native-worklets'; 2 | 3 | function getWorkletContext(): IWorkletContext { 4 | const ctx = global.__wishlistWorkletContext; 5 | if (!ctx) { 6 | throw new Error('Worklet context not initialized'); 7 | } 8 | return ctx; 9 | } 10 | 11 | export function createRunInWishlistFn< 12 | C extends ContextType, 13 | T, 14 | A extends Array, 15 | >(fn: (this: C, ...args: A) => T): (...args: A) => Promise { 16 | return Worklets.createRunInContextFn(fn, getWorkletContext()); 17 | } 18 | 19 | export const createRunInJsFn = Worklets.createRunInJsFn; 20 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:metro-react-native-babel-preset'], 6 | plugins: [ 7 | [ 8 | 'react-native-worklets/plugin', 9 | { 10 | globals: ['_log', '_chronoNow'], 11 | functionsToWorkletize: [{ name: 'useTemplateValue', args: [0] }], 12 | }, 13 | ], 14 | [ 15 | 'module-resolver', 16 | { 17 | extensions: ['.tsx', '.ts', '.js', '.json'], 18 | alias: { 19 | [pak.name]: path.join(__dirname, '..', pak.source), 20 | }, 21 | }, 22 | ], 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /cpp/TemplateContainer/MGTemplateContainerState.cpp: -------------------------------------------------------------------------------- 1 | #include "MGTemplateContainerState.h" 2 | 3 | #ifdef ANDROID 4 | #include "JNIStateRegistry.h" 5 | #endif 6 | 7 | namespace facebook::react { 8 | 9 | const std::vector> 10 | &MGTemplateContainerState::getTemplates() const { 11 | return templates_; 12 | } 13 | 14 | #ifdef ANDROID 15 | 16 | folly::dynamic MGTemplateContainerState::getDynamic() const { 17 | auto templatesRef = 18 | Wishlist::JNIStateRegistry::getInstance().addValue((void *)&templates_); 19 | return folly::dynamic::object("templates", templatesRef); 20 | }; 21 | 22 | #endif 23 | 24 | } // namespace facebook::react 25 | -------------------------------------------------------------------------------- /cpp/DependencyInjection/MGUIManagerHolder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using namespace facebook::react; 6 | 7 | namespace Wishlist { 8 | 9 | class MGUIManagerHolder { 10 | public: 11 | static MGUIManagerHolder &getInstance(); 12 | 13 | std::shared_ptr getUIManager() const; 14 | void setUIManager(const std::shared_ptr &uiManager); 15 | 16 | private: 17 | MGUIManagerHolder(); 18 | MGUIManagerHolder(const MGUIManagerHolder &) = delete; 19 | MGUIManagerHolder &operator=(const MGUIManagerHolder &) = delete; 20 | 21 | std::shared_ptr uiManager_; 22 | }; 23 | 24 | }; // namespace Wishlist 25 | -------------------------------------------------------------------------------- /src/Components/ForEach.tsx: -------------------------------------------------------------------------------- 1 | import { renderTemplate } from '../renderTemplate'; 2 | import { createTemplateComponent } from '../createTemplateComponent'; 3 | import { ForEachBase } from './ForEachBase'; 4 | 5 | export const ForEach = createTemplateComponent(ForEachBase, { 6 | addProps: (item, props, inflatorId, pool, rootValue) => { 7 | 'worklet'; 8 | 9 | const subItems: unknown[] = props.items; 10 | const items = subItems.map((subItem) => { 11 | return renderTemplate( 12 | props.template, 13 | subItem, 14 | rootValue, 15 | inflatorId, 16 | pool, 17 | ); 18 | }); 19 | 20 | item.setChildren(items); 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /android/src/main/jni/UISchedulerAndroid.cpp: -------------------------------------------------------------------------------- 1 | #include "UISchedulerAndroid.h" 2 | 3 | #include 4 | 5 | namespace Wishlist { 6 | 7 | using namespace facebook::jni; 8 | 9 | void UISchedulerAndroid::scheduleOnUI(std::function &&f) { 10 | // TODO: Our thread should have access to JVM by default. 11 | ThreadScope::WithClassLoader([&] { 12 | static const auto cls = javaClassStatic(); 13 | static const auto method = 14 | cls->getStaticMethod("scheduleOnUI"); 15 | auto jrunnable = JNativeRunnable::newObjectCxxArgs(std::move(f)); 16 | method(cls, jrunnable.get()); 17 | }); 18 | } 19 | 20 | }; // namespace Wishlist 21 | -------------------------------------------------------------------------------- /ios/MGWishListComponent.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #include 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | @interface MGWishListComponent : RCTScrollViewComponentView 9 | 10 | - (void)setTemplates:(std::vector>)templates 11 | withNames:(std::vector)names; 12 | - (void)setInflatorId:(std::string)inflatorId; 13 | - (void)setWishlistId:(std::string)wishlistId; 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /src/__tests__/WishlistData-test.tsx: -------------------------------------------------------------------------------- 1 | import { createItemsDataStructure } from '../WishlistDataCopy'; 2 | 3 | describe('allow negative indices', () => { 4 | it('adding new item should change index of old element', () => { 5 | const wishlistData = createItemsDataStructure([ 6 | { key: 'b', value: 'val1' }, 7 | { key: 'c', value: 'val2' }, 8 | ]); 9 | const indexOfB = wishlistData.getIndex('b'); 10 | 11 | wishlistData.unshift({ key: 'a', value: 'val0' }); 12 | 13 | const indexOfBAfterChanges = wishlistData.getIndex('b'); 14 | const indexOfA = wishlistData.getIndex('a'); 15 | 16 | expect(indexOfB).toBe(indexOfBAfterChanges); 17 | expect(indexOfA).toBe(-1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | ext { 5 | buildToolsVersion = "33.0.0" 6 | minSdkVersion = 21 7 | compileSdkVersion = 33 8 | targetSdkVersion = 33 9 | 10 | // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. 11 | ndkVersion = "25.1.8937393" 12 | 13 | kotlinVersion = "1.6.21" 14 | } 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | dependencies { 20 | classpath("com.android.tools.build:gradle") 21 | classpath("com.facebook.react:react-native-gradle-plugin") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Orchestrator/MGOrchestratorCppAdapter.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGOrchestratorCppAdapter.cpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #include "MGOrchestratorCppAdapter.hpp" 9 | 10 | namespace Wishlist { 11 | 12 | MGOrchestratorCppAdapter::MGOrchestratorCppAdapter( 13 | std::function onRequestVSync, 14 | std::function items)> didPushChildren) 15 | : onRequestVSync_(onRequestVSync), didPushChildren_(didPushChildren) {} 16 | 17 | void MGOrchestratorCppAdapter::didPushChildren(std::vector newWindow) { 18 | didPushChildren_(std::move(newWindow)); 19 | } 20 | 21 | void MGOrchestratorCppAdapter::requestVSync() { 22 | onRequestVSync_(); 23 | } 24 | 25 | }; // namespace Wishlist 26 | -------------------------------------------------------------------------------- /cpp/ContentContainer/MGContentContainerShadowNode.cpp: -------------------------------------------------------------------------------- 1 | #include "MGContentContainerShadowNode.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | extern const char MGContentContainerComponentName[] = "MGContentContainer"; 10 | 11 | void MGContentContainerShadowNode::setWishlistChildren( 12 | const ShadowNode::SharedListOfShared &wishlistChildren) { 13 | auto state = getStateData(); 14 | if (state.wishlistChildren != wishlistChildren) { 15 | state.wishlistChildren = wishlistChildren; 16 | setStateData(std::move(state)); 17 | } 18 | } 19 | 20 | } // namespace react 21 | } // namespace facebook 22 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/WishlistPackage.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.react.ReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.uimanager.ViewManager 7 | 8 | class WishlistPackage : ReactPackage { 9 | override fun createNativeModules(reactContext: ReactApplicationContext): List { 10 | return listOf(WishlistManagerModule(reactContext)) 11 | } 12 | 13 | override fun createViewManagers(reactContext: ReactApplicationContext): List> { 14 | return listOf( 15 | WishlistViewManager(), 16 | TemplateContainerViewManager(), 17 | TemplateInterceptorViewManager(), 18 | ContentContainerViewManager()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /cpp/DataBinding/MGDataBindingImpl.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGDataBindingImpl.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 16/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include "MGDI.hpp" 14 | #include "MGDataBinding.hpp" 15 | 16 | namespace Wishlist { 17 | 18 | struct MGDataBindingImpl : MGDataBinding { 19 | std::weak_ptr di; 20 | std::string _wishlistId; 21 | 22 | MGDataBindingImpl( 23 | const std::string &wishlistId, 24 | const std::weak_ptr &di); 25 | 26 | virtual std::set applyChangesAndGetDirtyIndices( 27 | std::pair windowIndexRange); 28 | 29 | void registerBindings(); 30 | void unregisterBindings(); 31 | 32 | virtual ~MGDataBindingImpl(); 33 | }; 34 | 35 | }; // namespace Wishlist 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "react-native-wishlist": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "jsx": "react", 12 | "lib": ["esnext"], 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitReturns": true, 17 | "noImplicitUseStrict": false, 18 | "noStrictGenericChecks": false, 19 | "noUncheckedIndexedAccess": false, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "skipLibCheck": true, 24 | "strict": true, 25 | "target": "esnext" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Components/WishlistImage.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Platform } from 'react-native'; 2 | import { createTemplateComponent } from '../createTemplateComponent'; 3 | 4 | export const WishlistImage = createTemplateComponent(Image, { 5 | addProps: (item, props) => { 6 | 'worklet'; 7 | if (Platform.OS === 'android') { 8 | const { source, ...rest } = props; 9 | const src = source != null && !Array.isArray(source) ? [source] : source; 10 | item.addProps({ 11 | src, 12 | ...rest, 13 | }); 14 | } else { 15 | item.addProps(props); 16 | } 17 | }, 18 | additionalTemplateProps: [ 19 | 'style.borderRadius', 20 | 'style.borderTopLeftRadius', 21 | 'style.borderTopRightRadius', 22 | 'style.borderBottomLeftRadius', 23 | 'style.borderBottomRightRadius', 24 | ], 25 | }); 26 | -------------------------------------------------------------------------------- /example/android/app/src/release/java/com/wishlistexample/ReactNativeFlipper.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Meta Platforms, Inc. and affiliates. 3 | * 4 | *

This source code is licensed under the MIT license found in the LICENSE file in the root 5 | * directory of this source tree. 6 | */ 7 | package com.wishlistexample; 8 | 9 | import android.content.Context; 10 | import com.facebook.react.ReactInstanceManager; 11 | 12 | /** 13 | * Class responsible of loading Flipper inside your React Native application. This is the release 14 | * flavor of it so it's empty as we don't want to load Flipper. 15 | */ 16 | public class ReactNativeFlipper { 17 | public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { 18 | // Do nothing as we don't want to initialize Flipper on Release. 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/ios/WishlistExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/src/AssetList/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View, Image } from 'react-native'; 3 | 4 | type HeaderProps = {}; 5 | 6 | const VITALIK_AVATAR = 7 | 'https://gateway.ipfs.io/ipfs/QmSP4nq9fnN9dAiCj42ug9Wa79rqmQerZXZch82VqpiH7U/image.gif'; 8 | 9 | export function Header({}: HeaderProps) { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | 17 | const styles = StyleSheet.create({ 18 | container: { 19 | height: 108, 20 | paddingTop: 58, 21 | paddingHorizontal: 19, 22 | flexDirection: 'row', 23 | alignItems: 'center', 24 | justifyContent: 'space-between', 25 | }, 26 | avatar: { 27 | width: 34, 28 | height: 34, 29 | borderRadius: 17, 30 | backgroundColor: '#9DA0A8', 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/TemplateInterceptor.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import android.content.Context 4 | import android.view.View 5 | import android.view.ViewGroup 6 | import com.facebook.react.views.view.ReactViewGroup 7 | 8 | class TemplateInterceptor(reactContext: Context) : 9 | ReactViewGroup(reactContext), ViewGroup.OnHierarchyChangeListener { 10 | private var wishlist: Wishlist? = null 11 | 12 | init { 13 | setOnHierarchyChangeListener(this) 14 | } 15 | 16 | override fun onChildViewAdded(parent: View, child: View) { 17 | if (child is Wishlist) { 18 | wishlist = child 19 | } else if (child is TemplateContainer && wishlist != null) { 20 | child.wishlist = wishlist 21 | } 22 | } 23 | 24 | override fun onChildViewRemoved(parent: View, child: View) { 25 | if (child is Wishlist) { 26 | wishlist = null 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/src/AssetList/AssetListHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View } from 'react-native'; 3 | 4 | export function AssetListHeader() { 5 | return ( 6 | 7 | vitalik.eth 8 | 9 | $2,773,494.65 10 | 11 | ); 12 | } 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | paddingHorizontal: 19, 17 | paddingVertical: 5, 18 | height: 50, 19 | flexDirection: 'row', 20 | justifyContent: 'space-between', 21 | alignItems: 'center', 22 | }, 23 | name: { 24 | fontSize: 24, 25 | fontWeight: '800', 26 | letterSpacing: 0.4, 27 | textAlign: 'left', 28 | }, 29 | balance: { 30 | fontSize: 24, 31 | fontWeight: '600', 32 | letterSpacing: 0.4, 33 | textAlign: 'right', 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const os = require('os'); 2 | const path = require('path'); 3 | const child_process = require('child_process'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | const args = process.argv.slice(2); 7 | const options = { 8 | cwd: process.cwd(), 9 | env: process.env, 10 | stdio: 'inherit', 11 | encoding: 'utf-8', 12 | }; 13 | 14 | if (os.type() === 'Windows_NT') { 15 | options.shell = true; 16 | } 17 | 18 | let result; 19 | 20 | if (process.cwd() !== root || args.length) { 21 | // We're not in the root of the project, or additional arguments were passed 22 | // In this case, forward the command to `yarn` 23 | result = child_process.spawnSync('yarn', args, options); 24 | } else { 25 | // If `yarn` is run without arguments, perform bootstrap 26 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 27 | } 28 | 29 | process.exitCode = result.status; 30 | -------------------------------------------------------------------------------- /cpp/ContentContainer/MGContentContainerState.cpp: -------------------------------------------------------------------------------- 1 | #include "MGContentContainerState.h" 2 | 3 | namespace facebook { 4 | namespace react { 5 | 6 | MGContentContainerState::MGContentContainerState() 7 | : wishlistChildren(nullptr){}; 8 | 9 | MGContentContainerState::MGContentContainerState( 10 | const ShadowNode::SharedListOfShared &wishlistChildren) 11 | : wishlistChildren(wishlistChildren) {} 12 | 13 | #ifdef ANDROID 14 | 15 | MGContentContainerState::MGContentContainerState( 16 | MGContentContainerState const &previousState, 17 | folly::dynamic data) 18 | : wishlistChildren(previousState.wishlistChildren){}; 19 | 20 | folly::dynamic MGContentContainerState::getDynamic() const { 21 | return folly::dynamic::object; 22 | }; 23 | 24 | MapBuffer MGContentContainerState::getMapBuffer() const { 25 | return MapBufferBuilder::EMPTY(); 26 | }; 27 | 28 | #endif 29 | 30 | } // namespace react 31 | } // namespace facebook 32 | -------------------------------------------------------------------------------- /ios/Orchestrator/MGOrchestratorCppAdapter.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGOrchestratorCppAdapter.hpp 3 | // CocoaAsyncSocket 4 | // 5 | // Created by Szymon on 14/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include "MGVSyncRequester.hpp" 13 | #include "MGViewportCarerListener.hpp" 14 | 15 | namespace Wishlist { 16 | 17 | class MGOrchestratorCppAdapter final : public MGVSyncRequester, 18 | public MGViewportCarerListener { 19 | public: 20 | MGOrchestratorCppAdapter( 21 | std::function onRequestVSync, 22 | std::function items)> didPushChildren); 23 | 24 | private: 25 | void didPushChildren(std::vector newWindow) override; 26 | void requestVSync() override; 27 | 28 | std::function onRequestVSync_; 29 | std::function items)> didPushChildren_; 30 | }; 31 | 32 | }; // namespace Wishlist 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .classpath 35 | .cxx 36 | .gradle 37 | .idea 38 | .project 39 | .settings 40 | local.properties 41 | android.iml 42 | 43 | # Cocoapods 44 | # 45 | example/ios/Pods 46 | 47 | # Ruby 48 | example/vendor/ 49 | 50 | # node.js 51 | # 52 | node_modules/ 53 | npm-debug.log 54 | yarn-debug.log 55 | yarn-error.log 56 | 57 | # BUCK 58 | buck-out/ 59 | \.buckd/ 60 | android/app/libs 61 | android/keystores/debug.keystore 62 | 63 | # Expo 64 | .expo/* 65 | 66 | # generated by bob 67 | lib/ 68 | -------------------------------------------------------------------------------- /MGWishList.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "MGWishList" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.description = <<-DESC 10 | The fastest List component for React Native. 11 | DESC 12 | s.homepage = "https://github.com/margelo/react-native-wishlist" 13 | s.license = "MIT" 14 | # s.license = { :type => "MIT", :file => "FILE_LICENSE" } 15 | s.author = { "author" => "author@domain.cn" } 16 | s.platforms = { :ios => "13.4", :tvos => "13.4" } 17 | s.source = { :git => "https://github.com/margelo/react-native-wishlist.git", :tag => "#{s.version}" } 18 | 19 | s.source_files = [ 20 | "ios/**/*.{mm,h,m,cpp,hpp}", 21 | "cpp/**/*.{cpp,h,m,mm,hpp}", 22 | ] 23 | 24 | install_modules_dependencies(s) 25 | end 26 | 27 | -------------------------------------------------------------------------------- /cpp/DependencyInjection/MGDI.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGDI.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | #include "MGBindingProvider.hpp" 14 | #include "MGDataBinding.hpp" 15 | #include "MGErrorHandler.h" 16 | #include "MGUIScheduler.hpp" 17 | #include "MGVSyncRequester.hpp" 18 | #include "MGViewportCarer.hpp" 19 | #include "MGViewportCarerListener.hpp" 20 | 21 | namespace Wishlist { 22 | 23 | struct MGDI { 24 | virtual std::shared_ptr getDataBinding() = 0; 25 | virtual std::shared_ptr getVSyncRequester() = 0; 26 | virtual std::shared_ptr getViewportCarer() = 0; 27 | virtual std::shared_ptr getUIScheduler() = 0; 28 | virtual std::shared_ptr getErrorHandler() = 0; 29 | 30 | virtual ~MGDI() {} 31 | }; 32 | 33 | }; // namespace Wishlist 34 | -------------------------------------------------------------------------------- /cpp/ContentContainer/MGContentContainerShadowNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "MGContentContainerState.h" 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | JSI_EXPORT extern const char MGContentContainerComponentName[]; 12 | 13 | /* 14 | * `ShadowNode` for component. 15 | */ 16 | class JSI_EXPORT MGContentContainerShadowNode final 17 | : public ConcreteViewShadowNode< 18 | MGContentContainerComponentName, 19 | MGContentContainerProps, 20 | ViewEventEmitter, 21 | MGContentContainerState> { 22 | using ConcreteViewShadowNode::ConcreteViewShadowNode; 23 | 24 | public: 25 | void setWishlistChildren( 26 | const ShadowNode::SharedListOfShared &wishlistChildren); 27 | }; 28 | 29 | } // namespace react 30 | } // namespace facebook 31 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | # EXAMPLE USAGE 2 | # Refer for explanation to following link: 3 | # https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md 4 | # 5 | # pre-push: 6 | # commands: 7 | # packages-audit: 8 | # tags: frontend security 9 | # run: yarn audit 10 | # gems-audit: 11 | # tags: backend security 12 | # run: bundle audit 13 | # 14 | # pre-commit: 15 | # parallel: true 16 | # commands: 17 | # eslint: 18 | # glob: "*.{js,ts}" 19 | # run: yarn eslint {staged_files} 20 | # rubocop: 21 | # tags: backend style 22 | # glob: "*.rb" 23 | # exclude: "application.rb|routes.rb" 24 | # run: bundle exec rubocop --force-exclusion {all_files} 25 | # govet: 26 | # tags: backend style 27 | # files: git ls-files -m 28 | # glob: "*.go" 29 | # run: go vet {files} 30 | # scripts: 31 | # "hello.js": 32 | # runner: node 33 | # "any.go": 34 | # runner: go run 35 | -------------------------------------------------------------------------------- /cpp/ContentContainer/MGContentContainerState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef ANDROID 6 | #include 7 | #include 8 | #include 9 | #endif 10 | 11 | namespace facebook { 12 | namespace react { 13 | 14 | /* 15 | * State for component. 16 | */ 17 | class JSI_EXPORT MGContentContainerState final { 18 | public: 19 | ShadowNode::SharedListOfShared wishlistChildren; 20 | 21 | MGContentContainerState(); 22 | MGContentContainerState( 23 | const ShadowNode::SharedListOfShared &wishlistChildren); 24 | 25 | #ifdef ANDROID 26 | MGContentContainerState( 27 | MGContentContainerState const &previousState, 28 | folly::dynamic data); 29 | folly::dynamic getDynamic() const; 30 | MapBuffer getMapBuffer() const; 31 | #endif 32 | }; 33 | 34 | } // namespace react 35 | } // namespace facebook 36 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/ContentContainer.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.react.module.annotations.ReactModule 4 | import com.facebook.react.uimanager.ThemedReactContext 5 | import com.facebook.react.uimanager.ViewGroupManager 6 | import com.facebook.react.viewmanagers.MGContentContainerManagerDelegate 7 | import com.facebook.react.viewmanagers.MGContentContainerManagerInterface 8 | import com.facebook.react.views.view.ReactViewGroup 9 | 10 | @ReactModule(name = ContentContainerViewManager.REACT_CLASS) 11 | class ContentContainerViewManager : 12 | ViewGroupManager(), MGContentContainerManagerInterface { 13 | companion object { 14 | const val REACT_CLASS = "MGContentContainer" 15 | } 16 | 17 | override fun getName() = REACT_CLASS 18 | 19 | override fun createViewInstance(reactContext: ThemedReactContext) = ReactViewGroup(reactContext) 20 | 21 | override fun getDelegate() = MGContentContainerManagerDelegate(this) 22 | } 23 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/TemplateInterceptorViewManager.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.react.module.annotations.ReactModule 4 | import com.facebook.react.uimanager.ThemedReactContext 5 | import com.facebook.react.uimanager.ViewGroupManager 6 | import com.facebook.react.viewmanagers.MGTemplateInterceptorManagerDelegate 7 | import com.facebook.react.viewmanagers.MGTemplateInterceptorManagerInterface 8 | 9 | @ReactModule(name = TemplateInterceptorViewManager.REACT_CLASS) 10 | class TemplateInterceptorViewManager : 11 | ViewGroupManager(), 12 | MGTemplateInterceptorManagerInterface { 13 | companion object { 14 | const val REACT_CLASS = "MGTemplateInterceptor" 15 | } 16 | 17 | override fun getName() = REACT_CLASS 18 | 19 | override fun createViewInstance(reactContext: ThemedReactContext) = 20 | TemplateInterceptor(reactContext) 21 | 22 | override fun getDelegate() = MGTemplateInterceptorManagerDelegate(this) 23 | } 24 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WishlistExample", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android --active-arch-only", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "pods": "pod-install --quiet", 10 | "postinstall": "patch-package" 11 | }, 12 | "dependencies": { 13 | "react": "^18.2.0", 14 | "react-native": "^0.72.3", 15 | "react-native-gesture-handler": "^2.12.0", 16 | "react-native-worklets": "janicduplessis/react-native-worklets#react-native-worklets-v0.1.0-alpha.4-gitpkg" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.20.0", 20 | "@babel/runtime": "^7.20.0", 21 | "@react-native/metro-config": "^0.72.9", 22 | "metro-react-native-babel-preset": "0.76.7", 23 | "string-hash-64": "^1.0.3", 24 | "babel-plugin-module-resolver": "^4.1.0", 25 | "patch-package": "^6.4.7", 26 | "postinstall-postinstall": "^2.1.0" 27 | }, 28 | "engines": { 29 | "node": ">=16" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cpp/MGViewportCarer/MGViewportCarer.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGViewportCarer.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | namespace Wishlist { 14 | 15 | using namespace facebook::react; 16 | 17 | static float MG_NO_OFFSET = std::numeric_limits::min(); 18 | 19 | struct MGDims { 20 | float width; 21 | float height; 22 | }; 23 | 24 | class MGViewportCarer { 25 | public: 26 | virtual void initialRenderAsync( 27 | MGDims dimensions, 28 | float initialContentSize, 29 | int originItem, 30 | const std::vector> ®isteredViews, 31 | const std::vector &names, 32 | const std::string &inflatorId) = 0; 33 | 34 | virtual void didScrollAsync( 35 | MGDims dimensions, 36 | float contentOffset, 37 | const std::string &inflatorId) = 0; 38 | 39 | virtual void didUpdateContentOffset() = 0; 40 | }; 41 | 42 | }; // namespace Wishlist 43 | -------------------------------------------------------------------------------- /ios/MGWishlistQueue.m: -------------------------------------------------------------------------------- 1 | #import "MGWishlistQueue.h" 2 | 3 | dispatch_queue_t MGGetWishlistQueue(void) 4 | { 5 | static dispatch_queue_t wishlistQueue; 6 | static dispatch_once_t onceToken; 7 | dispatch_once(&onceToken, ^{ 8 | dispatch_queue_attr_t qos = 9 | dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1); 10 | wishlistQueue = dispatch_queue_create("wishlistqueue", qos); 11 | }); 12 | return wishlistQueue; 13 | } 14 | 15 | BOOL MGIsWishlistQueue(void) 16 | { 17 | static void *wishlistQueueKey = &wishlistQueueKey; 18 | static dispatch_once_t onceToken; 19 | dispatch_once(&onceToken, ^{ 20 | dispatch_queue_set_specific(MGGetWishlistQueue(), wishlistQueueKey, wishlistQueueKey, NULL); 21 | }); 22 | return dispatch_get_specific(wishlistQueueKey) == wishlistQueueKey; 23 | } 24 | 25 | void MGExecuteOnWishlistQueue(dispatch_block_t block) 26 | { 27 | if (MGIsWishlistQueue()) { 28 | block(); 29 | } else { 30 | dispatch_async(MGGetWishlistQueue(), ^{ 31 | block(); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ios/MGContentContainerComponent.mm: -------------------------------------------------------------------------------- 1 | #import "MGContentContainerComponent.h" 2 | 3 | #import 4 | #import 5 | #import "MGContentContainerComponentDescriptor.h" 6 | #import "RCTFabricComponentsPlugins.h" 7 | 8 | using namespace facebook::react; 9 | 10 | @implementation MGContentContainerComponent 11 | 12 | - (instancetype)initWithFrame:(CGRect)frame 13 | { 14 | if (self = [super initWithFrame:frame]) { 15 | static const auto defaultProps = std::make_shared(); 16 | _props = defaultProps; 17 | } 18 | 19 | return self; 20 | } 21 | 22 | #pragma mark - RCTComponentViewProtocol 23 | 24 | + (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider 25 | { 26 | return facebook::react::concreteComponentDescriptorProvider(); 27 | } 28 | 29 | @end 30 | 31 | Class MGContentContainerCls(void) 32 | { 33 | return MGContentContainerComponent.class; 34 | } 35 | -------------------------------------------------------------------------------- /cpp/Wishlist/MGWishlistState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #ifdef ANDROID 7 | #include 8 | #include 9 | #include 10 | #endif 11 | 12 | #include "ComponentsPool.h" 13 | #include "MGViewportCarerImpl.h" 14 | 15 | using namespace Wishlist; 16 | 17 | namespace facebook { 18 | namespace react { 19 | 20 | // class WishlistShadowNode; 21 | 22 | /* 23 | * State for component. 24 | */ 25 | class JSI_EXPORT MGWishlistState final { 26 | public: 27 | bool initialised; 28 | std::shared_ptr viewportCarer; 29 | Rect contentBoundingRect; 30 | float contentOffset; 31 | 32 | MGWishlistState(); 33 | 34 | #ifdef ANDROID 35 | MGWishlistState(MGWishlistState const &previousState, folly::dynamic data); 36 | folly::dynamic getDynamic() const; 37 | MapBuffer getMapBuffer() const; 38 | #endif 39 | }; 40 | 41 | } // namespace react 42 | } // namespace facebook 43 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ComponentPool.ts: -------------------------------------------------------------------------------- 1 | import { TemplateItem } from './TemplateItem'; 2 | 3 | export type NodeType = 'View' | 'Text' | 'Paragraph' | 'RawText'; 4 | 5 | export type NativeComponentPool = { 6 | getComponent: (id: string) => TemplateItem | undefined; 7 | }; 8 | 9 | export type ComponentPool = NativeComponentPool & { 10 | createNode(type: NodeType): TemplateItem; 11 | }; 12 | 13 | export function wrapComponentPool(pool: NativeComponentPool): ComponentPool { 14 | 'worklet'; 15 | 16 | return { 17 | getComponent: pool.getComponent, 18 | createNode(type) { 19 | switch (type) { 20 | case 'Paragraph': 21 | return pool.getComponent('__paragraphComponent')!.Paragraph!; 22 | case 'Text': 23 | return pool.getComponent('__textComponent')!.Paragraph!.Text!; 24 | case 'RawText': 25 | return pool.getComponent('__paragraphComponent')!.Paragraph!.RawText!; 26 | case 'View': 27 | return pool.getComponent('__viewComponent')!; 28 | default: 29 | throw new Error(`Unknown node type ${type}`); 30 | } 31 | }, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Margelo GmbH 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /cpp/Wishlist/MGWishlistShadowNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "MGWishlistState.h" 9 | 10 | namespace facebook { 11 | namespace react { 12 | 13 | JSI_EXPORT extern const char MGWishlistComponentName[]; 14 | 15 | /* 16 | * `ShadowNode` for component. 17 | */ 18 | class JSI_EXPORT MGWishlistShadowNode 19 | : public ConcreteViewShadowNode< 20 | MGWishlistComponentName, 21 | MGWishlistProps, 22 | MGWishlistEventEmitter, 23 | MGWishlistState>, 24 | public std::enable_shared_from_this { 25 | using ConcreteViewShadowNode::ConcreteViewShadowNode; 26 | 27 | public: 28 | void layout(LayoutContext layoutContext) override; 29 | 30 | void updateContentOffset(float contentOffset); 31 | 32 | private: 33 | void updateStateIfNeeded(); 34 | }; 35 | 36 | } // namespace react 37 | } // namespace facebook 38 | -------------------------------------------------------------------------------- /example/ios/WishlistExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cpp/TemplateContainer/MGTemplateContainerState.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #ifdef ANDROID 7 | #include 8 | #include 9 | #include 10 | #endif 11 | 12 | namespace facebook { 13 | namespace react { 14 | 15 | class JSI_EXPORT MGTemplateContainerState final { 16 | public: 17 | MGTemplateContainerState(){}; 18 | MGTemplateContainerState( 19 | std::vector> const &templates) 20 | : templates_(templates){}; 21 | 22 | const std::vector> &getTemplates() const; 23 | 24 | #ifdef ANDROID 25 | MGTemplateContainerState( 26 | MGTemplateContainerState const &previousState, 27 | folly::dynamic data){}; 28 | 29 | folly::dynamic getDynamic() const; 30 | 31 | MapBuffer getMapBuffer() const { 32 | return MapBufferBuilder::EMPTY(); 33 | }; 34 | #endif 35 | 36 | private: 37 | std::vector> templates_; 38 | }; 39 | 40 | } // namespace react 41 | } // namespace facebook 42 | -------------------------------------------------------------------------------- /example/ios/WishlistExample/AppDelegate.mm: -------------------------------------------------------------------------------- 1 | #import "AppDelegate.h" 2 | 3 | #import 4 | #import 5 | #import "MGWishlistManager.h" 6 | 7 | @implementation AppDelegate 8 | 9 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 10 | { 11 | self.moduleName = @"WishlistExample"; 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = @{}; 15 | 16 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 17 | } 18 | 19 | - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge 20 | { 21 | #if DEBUG 22 | return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; 23 | #else 24 | return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 25 | #endif 26 | } 27 | 28 | - (Class)getModuleClassFromName:(const char *)name 29 | { 30 | if (strcmp(name, "WishlistManager") == 0) { 31 | return MGWishlistManager.class; 32 | } 33 | return RCTCoreModulesClassProvider(name); 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/TemplateContainer.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import android.content.Context 4 | import com.facebook.react.uimanager.FabricViewStateManager 5 | import com.facebook.react.uimanager.FabricViewStateManager.HasFabricViewStateManager 6 | import com.facebook.react.views.view.ReactViewGroup 7 | 8 | class TemplateContainer(reactContext: Context) : 9 | ReactViewGroup(reactContext), HasFabricViewStateManager { 10 | var inflatorId: String? = null 11 | var wishlistId: String? = null 12 | var names: List? = null 13 | var wishlist: Wishlist? = null 14 | set(value) { 15 | field = value 16 | updateWishlist() 17 | } 18 | private val fabricViewStateManager: FabricViewStateManager = FabricViewStateManager() 19 | 20 | override fun getFabricViewStateManager() = fabricViewStateManager 21 | 22 | fun updateWishlist() { 23 | wishlist?.let { 24 | it.wishlistId = wishlistId 25 | it.inflatorId = inflatorId 26 | val templatesRef = fabricViewStateManager.stateData?.getInt("templates") 27 | if (templatesRef != null) { 28 | it.setTemplates(templatesRef, names ?: listOf()) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-wishlist 2 | 3 | The fastest List component for React Native. 4 | 5 | 6 | ```jsx 7 | function ChatRoom({ room }) { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | ``` 20 | 21 | ## State of WishList 22 | 23 | WishList is an archived, read-only repository, and should probably not be used in production. It's a good proof of concept, and a pretty impressive experiment. See [this Notion document](https://margelo.notion.site/WishList-Summit-b20c24d1f0da4889a0513dfa929be5ed?pvs=74) for more details. 24 | 25 | ## Installation 26 | 27 | ```sh 28 | yarn add react-native-worklets # still private 29 | yarn add react-native-wishlist 30 | cd ios && pod install 31 | ``` 32 | 33 | ## Usage 34 | 35 | See [USAGE.md](./USAGE.md) 36 | 37 | ## Contributing 38 | 39 | See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. 40 | 41 | ## License 42 | 43 | MIT 44 | -------------------------------------------------------------------------------- /example/src/Chat/ReactionPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; 3 | import { emoticons } from './Data'; 4 | 5 | type ReactionPickerProps = { 6 | onPickReaction: (emoji: string) => void; 7 | }; 8 | 9 | export function ReactionPicker({ onPickReaction }: ReactionPickerProps) { 10 | return ( 11 | 12 | 13 | {emoticons.map((emoji, index) => ( 14 | onPickReaction(emoji)} 16 | key={String(index)} 17 | > 18 | {emoji} 19 | 20 | ))} 21 | 22 | 23 | ); 24 | } 25 | 26 | const styles = StyleSheet.create({ 27 | backdrop: { 28 | ...StyleSheet.absoluteFillObject, 29 | backgroundColor: 'rgba(0,0,0,0.5)', 30 | justifyContent: 'center', 31 | alignItems: 'center', 32 | }, 33 | container: { 34 | flexDirection: 'row', 35 | padding: 24, 36 | backgroundColor: 'white', 37 | borderRadius: 24, 38 | }, 39 | emoji: { 40 | fontSize: 38, 41 | paddingHorizontal: 8, 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/Orchestrator.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.jni.HybridData 4 | import com.facebook.proguard.annotations.DoNotStrip 5 | import com.facebook.react.uimanager.PixelUtil 6 | 7 | class Orchestrator(private val mWishlist: Wishlist, wishlistId: String, viewportCarerRef: Int) { 8 | companion object { 9 | init { 10 | WishlistSoLoader.staticInit() 11 | } 12 | } 13 | 14 | @field:DoNotStrip private val mHybridData = initHybrid(wishlistId, viewportCarerRef) 15 | 16 | private external fun initHybrid(wishlistId: String, viewportCarerRef: Int): HybridData 17 | 18 | external fun renderAsync( 19 | width: Float, 20 | height: Float, 21 | initialContentSize: Float, 22 | originItem: Int, 23 | templatesRef: Int, 24 | names: List, 25 | inflatorId: String 26 | ) 27 | 28 | external fun didScrollAsync(width: Float, height: Float, contentOffset: Float, inflatorId: String) 29 | 30 | external fun scrollToItem(index: Int) 31 | 32 | external fun didUpdateContentOffset() 33 | 34 | @DoNotStrip 35 | private fun scrollToOffset(offset: Float, animated: Boolean) { 36 | mWishlist.reactSmoothScrollTo(0, PixelUtil.toPixelFromDIP(offset).toInt()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cpp/WishlistJsRuntime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace facebook; 11 | 12 | namespace Wishlist { 13 | 14 | class WishlistJsRuntime { 15 | public: 16 | static WishlistJsRuntime &getInstance(); 17 | 18 | void initialize( 19 | jsi::Runtime *runtime, 20 | std::function &&)> jsCallInvoker, 21 | std::function &&)> workletCallInvoker); 22 | 23 | jsi::Runtime &getRuntime() const; 24 | void accessRuntime(std::function &&f) const; 25 | void accessRuntimeSync(std::function &&f) const; 26 | 27 | private: 28 | WishlistJsRuntime(); 29 | WishlistJsRuntime(const WishlistJsRuntime &) = delete; 30 | WishlistJsRuntime &operator=(const WishlistJsRuntime &) = delete; 31 | 32 | class Decorator : public RNWorklet::JsiBaseDecorator { 33 | void decorateRuntime(jsi::Runtime &runtime) override; 34 | }; 35 | 36 | std::shared_ptr workletContext_; 37 | }; 38 | 39 | }; // namespace Wishlist 40 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wishlistexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wishlistexample; 2 | 3 | import com.facebook.react.ReactActivity; 4 | import com.facebook.react.ReactActivityDelegate; 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate; 7 | 8 | public class MainActivity extends ReactActivity { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | @Override 15 | protected String getMainComponentName() { 16 | return "WishlistExample"; 17 | } 18 | 19 | /** 20 | * Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link 21 | * DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React 22 | * (aka React 18) with two boolean flags. 23 | */ 24 | @Override 25 | protected ReactActivityDelegate createReactActivityDelegate() { 26 | return new DefaultReactActivityDelegate( 27 | this, 28 | getMainComponentName(), 29 | // If you opted-in for the New Architecture, we enable the Fabric Renderer. 30 | DefaultNewArchitectureEntryPoint.getFabricEnabled()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Specs/NativeWishlist.ts: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | import type { ViewProps } from 'react-native'; 3 | import type { 4 | DirectEventHandler, 5 | Double, 6 | Int32, 7 | } from 'react-native/Libraries/Types/CodegenTypes'; 8 | import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands'; 9 | import codegenNativeComponent, { 10 | NativeComponentType, 11 | } from 'react-native/Libraries/Utilities/codegenNativeComponent'; 12 | 13 | export type EventInFile = Readonly<{ 14 | value: Double; 15 | }>; 16 | 17 | export interface WishlistProps extends ViewProps { 18 | inflatorId: string; 19 | initialIndex: Int32; 20 | onStartReached?: DirectEventHandler>; 21 | onEndReached?: DirectEventHandler>; 22 | } 23 | 24 | type NativeType = NativeComponentType; 25 | 26 | export type ScrollToItem = ( 27 | viewRef: React.ElementRef, 28 | index: Int32, 29 | animated: boolean, 30 | ) => void; 31 | 32 | interface NativeCommands { 33 | readonly scrollToItem: ScrollToItem; 34 | } 35 | 36 | export const Commands = codegenNativeCommands({ 37 | supportedCommands: ['scrollToItem'], 38 | }); 39 | 40 | export default codegenNativeComponent('MGWishlist', { 41 | interfaceOnly: true, 42 | }); 43 | -------------------------------------------------------------------------------- /cpp/Wishlist/MGWishlistState.cpp: -------------------------------------------------------------------------------- 1 | #include "MGWishlistState.h" 2 | 3 | #ifdef ANDROID 4 | #include "JNIStateRegistry.h" 5 | #endif 6 | 7 | namespace facebook { 8 | namespace react { 9 | 10 | MGWishlistState::MGWishlistState() 11 | : initialised(false), 12 | viewportCarer(std::make_shared()), 13 | contentBoundingRect({}), 14 | contentOffset(MG_NO_OFFSET){}; 15 | 16 | #ifdef ANDROID 17 | 18 | MGWishlistState::MGWishlistState( 19 | MGWishlistState const &previousState, 20 | folly::dynamic data) 21 | : initialised(previousState.initialised), 22 | viewportCarer(previousState.viewportCarer), 23 | contentBoundingRect(previousState.contentBoundingRect), 24 | contentOffset(MG_NO_OFFSET){}; 25 | 26 | folly::dynamic MGWishlistState::getDynamic() const { 27 | auto viewportCarerRef = Wishlist::JNIStateRegistry::getInstance().addValue( 28 | (void *)&viewportCarer); 29 | folly::dynamic result = folly::dynamic::object(); 30 | result["viewportCarer"] = viewportCarerRef; 31 | if (contentOffset != MG_NO_OFFSET) { 32 | result["contentOffset"] = contentOffset; 33 | } 34 | return result; 35 | }; 36 | 37 | MapBuffer MGWishlistState::getMapBuffer() const { 38 | return MapBufferBuilder::EMPTY(); 39 | }; 40 | 41 | #endif 42 | 43 | } // namespace react 44 | } // namespace facebook 45 | -------------------------------------------------------------------------------- /ios/Orchestrator/MGOrchestrator.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | #import 6 | #import 7 | #import "MGDI.hpp" 8 | #import "MGViewportCarerImpl.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | using namespace Wishlist; 13 | 14 | @class MGWishListComponent; 15 | 16 | @interface MGOrchestrator : NSObject 17 | 18 | - (instancetype)initWith:(MGWishListComponent *)wishlist 19 | wishlistId:(std::string)wishlistId 20 | viewportCarer:(std::shared_ptr)viewportCarer; 21 | 22 | - (void)renderAsyncWithDimensions:(MGDims)dimensions 23 | initialContentSize:(CGFloat)initialContentSize 24 | initialIndex:(NSInteger)initialIndex 25 | templates:(std::vector>)templates 26 | names:(std::vector)names 27 | inflatorId:(std::string)inflatorId; 28 | - (void)didScrollAsyncWithDimensions:(MGDims)dimensions 29 | contentOffset:(float)contentOffset 30 | inflatorId:(std::string)inflatorId; 31 | - (void)scrollToItem:(int)index; 32 | 33 | @end 34 | 35 | NS_ASSUME_NONNULL_END 36 | -------------------------------------------------------------------------------- /.github/workflows/build-android.yml: -------------------------------------------------------------------------------- 1 | name: Build Android 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '.github/workflows/build-android.yml' 9 | - 'android/**' 10 | - 'example/android/**' 11 | pull_request: 12 | paths: 13 | - '.github/workflows/build-android.yml' 14 | - 'android/**' 15 | - 'example/android/**' 16 | 17 | jobs: 18 | build: 19 | name: Build Android Example App 20 | runs-on: ubuntu-latest 21 | defaults: 22 | run: 23 | working-directory: example/android 24 | steps: 25 | - uses: actions/checkout@v2 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | - name: Restore node_modules from cache 31 | uses: actions/cache@v2 32 | id: yarn-cache 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | - name: Install node_modules 39 | run: yarn install --frozen-lockfile --cwd ../.. 40 | - name: Install node_modules for example/ 41 | run: yarn install --frozen-lockfile --cwd .. 42 | 43 | - name: Build App 44 | run: ./gradlew assembleDebug 45 | -------------------------------------------------------------------------------- /android/src/main/jni/WishlistManagerModule.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "ErrorHandlerAndroid.h" 10 | 11 | using namespace facebook::react; 12 | using namespace facebook::jni; 13 | 14 | namespace Wishlist { 15 | 16 | class WishlistManagerModule : public HybridClass { 17 | public: 18 | WishlistManagerModule(); 19 | ~WishlistManagerModule(); 20 | 21 | static constexpr auto kJavaDescriptor = 22 | "Lcom/wishlist/WishlistManagerModule;"; 23 | 24 | static void registerNatives(); 25 | 26 | private: 27 | static local_ref initHybrid(alias_ref); 28 | 29 | void nativeInstall( 30 | jlong jsiRuntimeRef, 31 | alias_ref jsCallInvokerHolder, 32 | alias_ref fabricUIManager); 33 | 34 | private: 35 | friend HybridBase; 36 | 37 | std::shared_ptr wishlistQueue_; 38 | std::shared_ptr scheduler_; 39 | std::shared_ptr eventListener_; 40 | std::shared_ptr errorHandler_; 41 | }; 42 | 43 | } // namespace Wishlist 44 | -------------------------------------------------------------------------------- /cpp/TemplateContainer/MGTemplateContainerShadowNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "MGTemplateContainerState.h" 9 | 10 | namespace facebook { 11 | namespace react { 12 | 13 | JSI_EXPORT extern const char MGTemplateContainerComponentName[]; 14 | 15 | /* 16 | * `ShadowNode` for component. 17 | */ 18 | class JSI_EXPORT MGTemplateContainerShadowNode final 19 | : public ConcreteViewShadowNode< 20 | MGTemplateContainerComponentName, 21 | MGTemplateContainerProps, 22 | MGTemplateContainerEventEmitter, 23 | MGTemplateContainerState> { 24 | using ConcreteViewShadowNode::ConcreteViewShadowNode; 25 | 26 | public: 27 | MGTemplateContainerShadowNode( 28 | ShadowNode const &sourceShadowNode, 29 | ShadowNodeFragment const &fragment); 30 | 31 | std::vector> templates; 32 | 33 | void appendChild(ShadowNode::Shared const &childNode) override; 34 | 35 | void layout(LayoutContext layoutContext) override; 36 | 37 | private: 38 | void updateStateIfNeeded(); 39 | }; 40 | 41 | } // namespace react 42 | } // namespace facebook 43 | -------------------------------------------------------------------------------- /src/EventHandler.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { createRunInWishlistFn } from './WishlistJsRuntime'; 3 | 4 | let done = false; 5 | const maybeInit = () => { 6 | if (!done) { 7 | done = true; 8 | createRunInWishlistFn(() => { 9 | 'worklet'; 10 | global.handlers = {}; 11 | 12 | global.handleEvent = (type: string, tag: number, event: any) => { 13 | // Events are prefixed with top sometimes. 14 | const key = tag.toString() + type.replace(/^topOn/, 'on'); 15 | const callback = global.handlers[key]; 16 | if (callback) { 17 | callback(event); 18 | } 19 | }; 20 | })(); 21 | } 22 | }; 23 | 24 | export type TemplateCallbackWorklet = ( 25 | nativeEvent: any, 26 | value: any, 27 | rootValue: any, 28 | ) => unknown; 29 | 30 | export class TemplateCallback { 31 | worklet: TemplateCallbackWorklet; 32 | eventName: string | undefined; 33 | 34 | constructor(worklet: TemplateCallbackWorklet, eventName?: string) { 35 | this.worklet = worklet; 36 | this.eventName = eventName; 37 | } 38 | } 39 | 40 | export function useTemplateCallback( 41 | worklet: (nativeEvent: any, value: any, rootValue: any) => unknown, 42 | eventName?: string, 43 | ) { 44 | return useMemo(() => { 45 | return new TemplateCallback(worklet, eventName); 46 | }, [worklet, eventName]); 47 | } 48 | 49 | export function initEventHandler() { 50 | maybeInit(); 51 | } 52 | -------------------------------------------------------------------------------- /cpp/Wishlist/MGWishlistShadowNode.cpp: -------------------------------------------------------------------------------- 1 | #include "MGWishlistShadowNode.h" 2 | 3 | namespace facebook { 4 | namespace react { 5 | 6 | extern const char MGWishlistComponentName[] = "MGWishlist"; 7 | 8 | void MGWishlistShadowNode::layout(LayoutContext layoutContext) { 9 | auto state = getStateData(); 10 | if (!state.initialised) { 11 | state.initialised = true; 12 | state.viewportCarer->setInitialValues(shared_from_this(), layoutContext); 13 | 14 | setStateData(std::move(state)); 15 | } 16 | 17 | ConcreteViewShadowNode::layout(layoutContext); 18 | 19 | // TODO update viewportObserver if needed 20 | 21 | updateStateIfNeeded(); 22 | } 23 | 24 | void MGWishlistShadowNode::updateStateIfNeeded() { 25 | ensureUnsealed(); 26 | 27 | auto contentBoundingRect = Rect{}; 28 | for (const auto &childNode : getLayoutableChildNodes()) { 29 | contentBoundingRect.unionInPlace(childNode->getLayoutMetrics().frame); 30 | } 31 | 32 | auto state = getStateData(); 33 | 34 | if (state.contentBoundingRect != contentBoundingRect) { 35 | state.contentBoundingRect = contentBoundingRect; 36 | setStateData(std::move(state)); 37 | } 38 | } 39 | 40 | void MGWishlistShadowNode::updateContentOffset(float contentOffset) { 41 | ensureUnsealed(); 42 | 43 | auto state = getStateData(); 44 | state.contentOffset = contentOffset; 45 | setStateData(std::move(state)); 46 | } 47 | 48 | } // namespace react 49 | } // namespace facebook 50 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ComponentsPool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "ShadowNodeBinding.h" 9 | #include "ShadowNodeCopyMachine.h" 10 | 11 | using namespace facebook::react; 12 | using namespace facebook::jsi; 13 | 14 | namespace Wishlist { 15 | 16 | class ComponentsPool : public std::enable_shared_from_this { 17 | public: 18 | void setNames(const std::vector &names); 19 | 20 | void setRegisteredViews(std::vector registeredViews); 21 | 22 | void returnToPool(ShadowNode::Shared sn); 23 | 24 | void templatesUpdated(); 25 | 26 | ShadowNode::Shared getNodeForType(const std::string &type); 27 | 28 | Object prepareProxy(Runtime &rt); 29 | 30 | class Proxy : public HostObject { 31 | public: 32 | std::weak_ptr wcp; 33 | 34 | Proxy(std::weak_ptr wcp); 35 | 36 | virtual Value get(Runtime &rt, const PropNameID &nameProp); 37 | 38 | virtual void set(Runtime &rt, const PropNameID &name, const Value &value); 39 | }; 40 | 41 | private: 42 | std::map nameToIndex_; 43 | std::map tagToType_; 44 | std::map> reusable_; 45 | std::vector registeredViews_; 46 | std::shared_ptr proxy_; 47 | }; 48 | 49 | }; // namespace Wishlist 50 | -------------------------------------------------------------------------------- /example/src/AssetList/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { TemplateValue, Wishlist } from 'react-native-wishlist'; 4 | 5 | type HeaderProps = { 6 | active: boolean; 7 | disabled?: boolean; 8 | text: TemplateValue | string; 9 | onPress: () => void; 10 | }; 11 | 12 | export function Button({ onPress, text, active, disabled }: HeaderProps) { 13 | return ( 14 | 15 | 22 | 25 | {text} 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | const styles = StyleSheet.create({ 33 | disabled: { 34 | opacity: 0.6, 35 | }, 36 | button: { 37 | borderRadius: 15, 38 | height: 30, 39 | backgroundColor: '#ccd0d9', 40 | paddingVertical: 5, 41 | paddingHorizontal: 10, 42 | justifyContent: 'center', 43 | alignItems: 'center', 44 | }, 45 | buttonActive: { 46 | backgroundColor: '#1F87FF', 47 | }, 48 | buttonText: { 49 | color: '#55585c', 50 | fontWeight: 'bold', 51 | fontSize: 16, 52 | lineHeight: 18, 53 | }, 54 | buttonTextActive: { 55 | color: 'white', 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const escape = require('escape-string-regexp'); 3 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | /** 14 | * Metro configuration 15 | * https://facebook.github.io/metro/docs/configuration 16 | * 17 | * @type {import('metro-config').MetroConfig} 18 | */ 19 | const config = { 20 | projectRoot: __dirname, 21 | watchFolders: [root], 22 | 23 | // We need to make sure that only one version is loaded for peerDependencies 24 | // So we block them at the root, and alias them to the versions in example's node_modules 25 | resolver: { 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`), 30 | ), 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | 39 | transformer: { 40 | getTransformOptions: async () => ({ 41 | transform: { 42 | experimentalImportSupport: false, 43 | inlineRequires: true, 44 | }, 45 | }), 46 | }, 47 | }; 48 | 49 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 50 | -------------------------------------------------------------------------------- /src/Components/Switch.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { View, ViewStyle } from 'react-native'; 3 | import { createTemplateComponent } from '../createTemplateComponent'; 4 | import type { TemplateValue } from '../TemplateValue'; 5 | 6 | const SwitchTemplateComponent = createTemplateComponent(View); 7 | 8 | type SwitchProps = { 9 | value: TemplateValue; 10 | children: React.ReactElement[]; 11 | style?: ViewStyle; 12 | }; 13 | 14 | export function Switch(props: SwitchProps) { 15 | const children = React.Children.map(props.children, (item) => 16 | React.cloneElement(item, { 17 | ...item.props, 18 | // @ts-expect-error this is hidden property 19 | switchValue: props.value, 20 | }), 21 | ); 22 | 23 | return ; 24 | } 25 | 26 | export const CaseBase = forwardRef((props, ref) => { 27 | return ; 28 | }); 29 | 30 | const CaseTemplateComponent = createTemplateComponent(CaseBase, { 31 | addProps: (item, props) => { 32 | 'worklet'; 33 | 34 | if (props.switchValue === props.value) { 35 | item.addProps({ display: 'flex' }); 36 | } else { 37 | item.addProps({ display: 'none' }); 38 | } 39 | }, 40 | }); 41 | 42 | type CaseProps = React.PropsWithChildren<{ 43 | value: TemplateValue | string | boolean | number; 44 | style?: ViewStyle; 45 | }>; 46 | 47 | export function Case(props: CaseProps) { 48 | return ; 49 | } 50 | -------------------------------------------------------------------------------- /cpp/ContentContainer/MGContentContainerComponentDescriptor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "MGContentContainerShadowNode.h" 7 | 8 | namespace facebook { 9 | namespace react { 10 | 11 | class MGContentContainerComponentDescriptor 12 | : public ConcreteComponentDescriptor { 13 | using ConcreteComponentDescriptor::ConcreteComponentDescriptor; 14 | 15 | ShadowNode::Unshared cloneShadowNode( 16 | const ShadowNode &sourceShadowNode, 17 | const ShadowNodeFragment &fragment) const override { 18 | // React holds on to old shadow nodes so we need to make sure to get the 19 | // latest state when cloning those to get the children that were set by 20 | // Wishlist. 21 | auto &mostRecentStateData = static_cast( 22 | sourceShadowNode.getMostRecentState().get()) 23 | ->getData(); 24 | auto shadowNode = std::make_shared( 25 | sourceShadowNode, 26 | ShadowNodeFragment{ 27 | fragment.props, 28 | mostRecentStateData.wishlistChildren 29 | ? mostRecentStateData.wishlistChildren 30 | : fragment.children, 31 | fragment.state}); 32 | 33 | adopt(shadowNode); 34 | return shadowNode; 35 | } 36 | }; 37 | 38 | } // namespace react 39 | } // namespace facebook 40 | -------------------------------------------------------------------------------- /cpp/TemplateContainer/MGTemplateContainerShadowNode.cpp: -------------------------------------------------------------------------------- 1 | #include "MGTemplateContainerShadowNode.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace facebook { 7 | namespace react { 8 | 9 | extern const char MGTemplateContainerComponentName[] = "MGTemplateContainer"; 10 | 11 | MGTemplateContainerShadowNode::MGTemplateContainerShadowNode( 12 | ShadowNode const &sourceShadowNode, 13 | ShadowNodeFragment const &fragment) 14 | : ConcreteViewShadowNode(sourceShadowNode, fragment) { 15 | auto &templateContainerSourceShadowNode = 16 | static_cast(sourceShadowNode); 17 | templates = templateContainerSourceShadowNode.templates; 18 | } 19 | 20 | void MGTemplateContainerShadowNode::appendChild( 21 | ShadowNode::Shared const &childNode) { 22 | this->templates.push_back(childNode); 23 | } 24 | 25 | void MGTemplateContainerShadowNode::layout(LayoutContext layoutContext) { 26 | updateStateIfNeeded(); 27 | ConcreteViewShadowNode::layout(layoutContext); 28 | } 29 | 30 | void MGTemplateContainerShadowNode::updateStateIfNeeded() { 31 | ensureUnsealed(); 32 | 33 | auto const ¤tState = getStateData(); 34 | if (this->templates == currentState.getTemplates()) { 35 | return; 36 | } 37 | 38 | auto state = MGTemplateContainerState{this->templates}; 39 | setStateData(std::move(state)); 40 | } 41 | 42 | } // namespace react 43 | } // namespace facebook 44 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/WishlistManagerModule.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.jni.HybridData 4 | import com.facebook.proguard.annotations.DoNotStrip 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.fabric.FabricUIManager 7 | import com.facebook.react.module.annotations.ReactModule 8 | import com.facebook.react.turbomodule.core.CallInvokerHolderImpl 9 | import com.facebook.react.uimanager.UIManagerHelper 10 | import com.facebook.react.uimanager.common.UIManagerType 11 | 12 | @ReactModule(name = WishlistManagerModule.NAME) 13 | class WishlistManagerModule(reactContext: ReactApplicationContext) : 14 | NativeWishlistManagerSpec(reactContext) { 15 | companion object { 16 | const val NAME = "WishlistManager" 17 | 18 | init { 19 | WishlistSoLoader.staticInit() 20 | } 21 | } 22 | 23 | @field:DoNotStrip private val mHybridData = initHybrid() 24 | 25 | override fun getName() = NAME 26 | 27 | override fun install(): Boolean { 28 | nativeInstall( 29 | reactApplicationContext.javaScriptContextHolder.get(), 30 | reactApplicationContext.catalystInstance.jsCallInvokerHolder as CallInvokerHolderImpl, 31 | UIManagerHelper.getUIManager(reactApplicationContext, UIManagerType.FABRIC) 32 | as FabricUIManager) 33 | return true 34 | } 35 | 36 | private external fun initHybrid(): HybridData 37 | 38 | private external fun nativeInstall( 39 | jsiRuntimeRef: Long, 40 | jsCallInvokerHolder: CallInvokerHolderImpl, 41 | fabricUIManager: FabricUIManager 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /cpp/DependencyInjection/MGDIImpl.hpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGDIImpl.hpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include "MGDI.hpp" 13 | 14 | namespace Wishlist { 15 | 16 | class MGDIImpl : public MGDI, public std::enable_shared_from_this { 17 | public: 18 | std::shared_ptr getDataBinding() override; 19 | std::shared_ptr getVSyncRequester() override; 20 | std::shared_ptr getViewportCarer() override; 21 | std::shared_ptr getUIScheduler() override; 22 | std::shared_ptr getErrorHandler() override; 23 | 24 | void setDataBinding(const std::shared_ptr &db); 25 | void setVSyncRequester(const std::shared_ptr &vr); 26 | void setPushChildrenListener( 27 | const std::shared_ptr &pcl); 28 | void setViewportCarer(const std::shared_ptr &vc); 29 | void setUIScheduler(const std::shared_ptr &us); 30 | void setErrorHandler(const std::shared_ptr &eh); 31 | 32 | std::weak_ptr getWeak(); 33 | 34 | virtual ~MGDIImpl(); 35 | 36 | private: 37 | std::shared_ptr dataBinding_; 38 | std::shared_ptr vSyncRequester_; 39 | std::shared_ptr pushChildrenListener_; 40 | std::shared_ptr viewportCarer_; 41 | std::shared_ptr uiScheduler_; 42 | std::shared_ptr errorHandler_; 43 | }; 44 | 45 | }; // namespace Wishlist 46 | -------------------------------------------------------------------------------- /example/src/AssetList/ItemCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { processColor, StyleSheet, View } from 'react-native'; 3 | import { useTemplateValue, Wishlist } from 'react-native-wishlist'; 4 | import type { AssetListItemWithState } from './AssetListExample'; 5 | 6 | const blue = processColor('#1F87FF'); 7 | const gray = processColor('#ccd0d9'); 8 | 9 | export function ItemCheckbox() { 10 | const checked = useTemplateValue( 11 | (item: AssetListItemWithState) => item.isSelected, 12 | ); 13 | 14 | const borderColor = useTemplateValue( 15 | (item: AssetListItemWithState) => 16 | (item.isSelected ? blue : gray) as any as string, 17 | ); 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | } 29 | 30 | const styles = StyleSheet.create({ 31 | container: { 32 | justifyContent: 'center', 33 | alignItems: 'center', 34 | marginLeft: 19, 35 | marginRight: 12, 36 | }, 37 | checked: { 38 | borderColor: '#1F87FF', 39 | }, 40 | outerCircle: { 41 | width: 24, 42 | height: 24, 43 | borderRadius: 12, 44 | // borderColor: '#ccd0d9', 45 | borderWidth: 2, 46 | backgroundColor: 'white', 47 | justifyContent: 'center', 48 | alignItems: 'center', 49 | }, 50 | innerCircle: { 51 | width: 14, 52 | height: 14, 53 | borderRadius: 7, 54 | backgroundColor: '#1F87FF', 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ShadowNodeCopyMachine.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace facebook; 11 | using namespace facebook::react; 12 | 13 | namespace Wishlist { 14 | 15 | class ShadowNodeCopyMachine { 16 | public: 17 | static ShadowNode::Shared copyShadowSubtree(const ShadowNode::Shared &sn); 18 | static void clearParent(const ShadowNode::Shared &sn); 19 | }; 20 | 21 | // dirty hack don't do it at home 22 | class ShadowNodeFamilyHack final { 23 | public: 24 | using Shared = std::shared_ptr; 25 | using Weak = std::weak_ptr; 26 | 27 | using AncestorList = butter::small_vector< 28 | std::pair< 29 | std::reference_wrapper /* parentNode */, 30 | int /* childIndex */>, 31 | 64>; 32 | 33 | mutable std::unique_ptr nativeProps_DEPRECATED; 34 | EventDispatcher::Weak eventDispatcher_; 35 | mutable std::shared_ptr mostRecentState_; 36 | mutable std::shared_mutex mutex_; 37 | Tag const tag_; 38 | SurfaceId const surfaceId_; 39 | SharedEventEmitter const eventEmitter_; 40 | ComponentDescriptor const &componentDescriptor_; 41 | ComponentHandle componentHandle_; 42 | ComponentName componentName_; 43 | mutable ShadowNodeFamily::Weak parent_{}; 44 | mutable bool hasParent_{false}; 45 | }; 46 | 47 | }; // namespace Wishlist 48 | -------------------------------------------------------------------------------- /cpp/DependencyInjection/MGDIImpl.cpp: -------------------------------------------------------------------------------- 1 | // 2 | // MGDIImpl.cpp 3 | // MGWishList 4 | // 5 | // Created by Szymon on 13/01/2023. 6 | // 7 | 8 | #include "MGDIImpl.hpp" 9 | 10 | #include 11 | 12 | namespace Wishlist { 13 | 14 | std::shared_ptr MGDIImpl::getDataBinding() { 15 | return dataBinding_; 16 | } 17 | 18 | std::shared_ptr MGDIImpl::getVSyncRequester() { 19 | return vSyncRequester_; 20 | } 21 | 22 | std::shared_ptr MGDIImpl::getViewportCarer() { 23 | return viewportCarer_; 24 | } 25 | 26 | std::shared_ptr MGDIImpl::getUIScheduler() { 27 | return uiScheduler_; 28 | }; 29 | 30 | std::shared_ptr MGDIImpl::getErrorHandler() { 31 | return errorHandler_; 32 | } 33 | 34 | void MGDIImpl::setDataBinding(const std::shared_ptr &db) { 35 | dataBinding_ = db; 36 | } 37 | 38 | void MGDIImpl::setVSyncRequester(const std::shared_ptr &vr) { 39 | vSyncRequester_ = vr; 40 | } 41 | 42 | void MGDIImpl::setPushChildrenListener( 43 | const std::shared_ptr &pcl) { 44 | pushChildrenListener_ = pcl; 45 | } 46 | 47 | void MGDIImpl::setViewportCarer(const std::shared_ptr &vc) { 48 | viewportCarer_ = vc; 49 | } 50 | 51 | void MGDIImpl::setUIScheduler(const std::shared_ptr &us) { 52 | uiScheduler_ = us; 53 | } 54 | 55 | void MGDIImpl::setErrorHandler(const std::shared_ptr &eh) { 56 | errorHandler_ = eh; 57 | } 58 | 59 | std::weak_ptr MGDIImpl::getWeak() { 60 | return weak_from_this(); 61 | } 62 | 63 | MGDIImpl::~MGDIImpl() {} 64 | 65 | }; // namespace Wishlist 66 | -------------------------------------------------------------------------------- /example/src/Chat/ChatList.tsx: -------------------------------------------------------------------------------- 1 | import React, { ForwardedRef } from 'react'; 2 | import type { ViewProps } from 'react-native'; 3 | import { 4 | Wishlist, 5 | WishlistData, 6 | WishListInstance, 7 | } from 'react-native-wishlist'; 8 | import { ChatItemView } from './ChatItem'; 9 | import type { ChatItem } from './Data'; 10 | import { LoadingView } from './LoadingView'; 11 | 12 | interface Props extends ViewProps { 13 | data: WishlistData; 14 | onAddReaction: (item: ChatItem) => void; 15 | intialIndex?: number; 16 | onStartReached?: () => void; 17 | onEndReached?: () => void; 18 | } 19 | 20 | export const ChatListView = React.memo( 21 | React.forwardRef( 22 | ( 23 | { 24 | data, 25 | intialIndex, 26 | onAddReaction, 27 | onStartReached, 28 | onEndReached, 29 | style, 30 | }: Props, 31 | ref: ForwardedRef, 32 | ) => { 33 | return ( 34 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | }, 54 | ), 55 | ); 56 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ItemProvider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include "ComponentsPool.h" 7 | #include "MGDI.hpp" 8 | 9 | using namespace facebook::react; 10 | 11 | namespace Wishlist { 12 | 13 | struct WishItem { 14 | float offset; 15 | int index; 16 | std::string key; 17 | std::string type; 18 | float height; 19 | bool dirty = false; 20 | std::shared_ptr sn; 21 | }; 22 | 23 | class ItemProvider { 24 | public: 25 | float maxWidth = 0; 26 | LayoutConstraints lcc; 27 | LayoutContext lc; 28 | 29 | ItemProvider(float maxWidth, LayoutContext lc) { 30 | this->maxWidth = maxWidth; 31 | this->lc = lc; 32 | lcc.layoutDirection = facebook::react::LayoutDirection::LeftToRight; 33 | lcc.maximumSize.width = maxWidth; 34 | } 35 | 36 | virtual void setComponentsPool(std::shared_ptr pool) = 0; 37 | 38 | virtual WishItem provide( 39 | int index, 40 | const std::shared_ptr &prevSn) = 0; 41 | 42 | virtual ~ItemProvider() {} 43 | }; 44 | 45 | struct WorkletItemProvider : ItemProvider { 46 | std::shared_ptr cp; 47 | std::string tag; 48 | std::weak_ptr di; 49 | 50 | WorkletItemProvider( 51 | std::weak_ptr di, 52 | float maxWidth, 53 | LayoutContext lc, 54 | std::string tag) 55 | : ItemProvider(maxWidth, lc), di(di) { 56 | this->tag = tag; 57 | } 58 | 59 | void setComponentsPool(std::shared_ptr pool) override { 60 | cp = pool; 61 | } 62 | 63 | WishItem provide(int index, const std::shared_ptr &prevSn) 64 | override; 65 | }; 66 | 67 | }; // namespace Wishlist 68 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ShadowNodeBinding.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "ShadowNodeCopyMachine.h" 9 | 10 | using namespace facebook; 11 | using namespace facebook::react; 12 | 13 | namespace Wishlist { 14 | 15 | class ComponentsPool; 16 | 17 | class ShadowNodeBinding 18 | : public jsi::HostObject, 19 | public std::enable_shared_from_this { 20 | public: 21 | ShadowNodeBinding( 22 | std::shared_ptr sn, 23 | std::weak_ptr wcp, 24 | const std::string &type, 25 | const std::string &key); 26 | 27 | ShadowNodeBinding( 28 | std::shared_ptr sn, 29 | std::weak_ptr wcp, 30 | std::shared_ptr parent); 31 | 32 | virtual jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &nameProp); 33 | 34 | virtual void 35 | set(jsi::Runtime &rt, const jsi::PropNameID &name, const jsi::Value &value); 36 | 37 | std::string getType() const; 38 | std::string getKey() const; 39 | ShadowNode::Shared getShadowNode() const; 40 | 41 | private: 42 | void describe( 43 | std::stringstream &ss, 44 | const std::shared_ptr n, 45 | int level); 46 | 47 | std::shared_ptr findNodeByWishId( 48 | const std::string &nativeId, 49 | std::shared_ptr p); 50 | 51 | private: 52 | std::shared_ptr sn_; 53 | std::weak_ptr wcp_; 54 | std::shared_ptr parent_; 55 | std::string type_; 56 | std::string key_; 57 | }; 58 | 59 | }; // namespace Wishlist 60 | -------------------------------------------------------------------------------- /.github/workflows/validate-js.yml: -------------------------------------------------------------------------------- 1 | name: Validate JS 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '.github/workflows/validate-js.yml' 9 | - 'src/**' 10 | - '*.json' 11 | - '*.js' 12 | - '*.lock' 13 | - 'example/src/**' 14 | - 'example/*.json' 15 | - 'example/*.js' 16 | - 'example/*.lock' 17 | - 'example/*.tsx' 18 | pull_request: 19 | paths: 20 | - '.github/workflows/validate-js.yml' 21 | - 'src/**' 22 | - '*.json' 23 | - '*.js' 24 | - '*.lock' 25 | - 'example/src/**' 26 | - 'example/*.json' 27 | - 'example/*.js' 28 | - 'example/*.lock' 29 | - 'example/*.tsx' 30 | 31 | jobs: 32 | main: 33 | name: Lint JS (prettier, eslint, typescript) 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - name: Get yarn cache directory path 39 | id: yarn-cache-dir-path 40 | run: echo "::set-output name=dir::$(yarn cache dir)" 41 | - name: Restore node_modules from cache 42 | uses: actions/cache@v2 43 | id: yarn-cache 44 | with: 45 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 46 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 47 | restore-keys: | 48 | ${{ runner.os }}-yarn- 49 | - name: Install node_modules 50 | run: yarn install --frozen-lockfile 51 | - name: Install node_modules (example/) 52 | run: yarn install --frozen-lockfile --cwd example 53 | 54 | - name: Check code formatting 55 | run: yarn format:check 56 | 57 | - name: Run ESLint 58 | run: yarn lint -f @jamesacarr/github-actions 59 | 60 | - name: Run TypeScript 61 | run: yarn typescript 62 | -------------------------------------------------------------------------------- /example/src/Chat/MessageInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { StyleSheet, View, Text, TextInput } from 'react-native'; 3 | import { BorderlessButton } from 'react-native-gesture-handler'; 4 | 5 | type MessageInputProps = { 6 | onSend: (text: string) => void; 7 | }; 8 | 9 | export function MessageInput({ onSend }: MessageInputProps) { 10 | const [value, setValue] = useState(''); 11 | 12 | const handlePress = () => { 13 | onSend(value); 14 | setValue(''); 15 | }; 16 | 17 | return ( 18 | 19 | 26 | 27 | Send 28 | 29 | 30 | ); 31 | } 32 | 33 | const styles = StyleSheet.create({ 34 | container: { 35 | height: 100, 36 | paddingBottom: 40, 37 | paddingHorizontal: 19, 38 | paddingTop: 12, 39 | flexDirection: 'row', 40 | justifyContent: 'space-between', 41 | }, 42 | input: { 43 | borderWidth: 1, 44 | borderColor: '#d6d6d6', 45 | borderRadius: 15, 46 | flex: 1, 47 | height: 35, 48 | paddingHorizontal: 12, 49 | fontSize: 15, 50 | }, 51 | button: { 52 | paddingLeft: 15, 53 | justifyContent: 'center', 54 | alignItems: 'center', 55 | height: 35, 56 | width: 84, 57 | }, 58 | text: { 59 | color: '#9DA0A8', 60 | fontWeight: 'bold', 61 | fontSize: 16, 62 | lineHeight: 18, 63 | }, 64 | active: { 65 | color: '#1F87FF', 66 | }, 67 | }); 68 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["Wishlist_kotlinVersion"] 3 | 4 | repositories { 5 | mavenCentral() 6 | google() 7 | } 8 | 9 | dependencies { 10 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 11 | classpath("com.android.tools.build:gradle:7.3.1") 12 | classpath("com.diffplug.spotless:spotless-plugin-gradle:6.11.0") 13 | } 14 | } 15 | 16 | if (project == rootProject) { 17 | apply from: "spotless.gradle" 18 | return 19 | } 20 | 21 | def getExtOrDefault(name) { 22 | return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["Wishlist_" + name] 23 | } 24 | 25 | def getExtOrIntegerDefault(name) { 26 | return getExtOrDefault(name).toInteger() 27 | } 28 | 29 | apply plugin: "com.android.library" 30 | apply plugin: "kotlin-android" 31 | apply plugin: "com.facebook.react" 32 | 33 | android { 34 | compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") 35 | ndkVersion getExtOrDefault("ndkVersion") 36 | 37 | defaultConfig { 38 | minSdkVersion getExtOrIntegerDefault("minSdkVersion") 39 | targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") 40 | } 41 | 42 | compileOptions { 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | 47 | buildFeatures { 48 | prefab true 49 | } 50 | } 51 | 52 | def kotlin_version = getExtOrDefault("kotlinVersion") 53 | 54 | 55 | dependencies { 56 | implementation "com.facebook.react:react-android" 57 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 58 | implementation project(":react-native-worklets") 59 | } 60 | -------------------------------------------------------------------------------- /example/ios/WishlistExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | WishlistExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSExceptionDomains 30 | 31 | localhost 32 | 33 | NSExceptionAllowsInsecureHTTPLoads 34 | 35 | 36 | 37 | 38 | NSLocationWhenInUseUsageDescription 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UIViewControllerBasedStatusBarAppearance 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /example/src/AssetList/AssetIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { useTemplateValue, Wishlist } from 'react-native-wishlist'; 4 | import type { AssetItemType } from './assets'; 5 | 6 | const ethIcon = require('./assets/eth.png'); 7 | // const optimismBadge = require('./assets/optimismBadge.png'); 8 | 9 | type AssetIconProps = {}; 10 | 11 | export function AssetIcon({}: AssetIconProps) { 12 | // TODO(terry): Figure out why local images won't work 13 | const iconSource = useTemplateValue((item: AssetItemType) => 14 | item.network === 'ETH' ? ethIcon : { uri: item.icon }, 15 | ); 16 | 17 | // TODO(terry): Use local image for badge 18 | // const badgeUri = useTemplateValue((item: AssetItemType) => 19 | // item.network === 'ETH' ? ethIcon : item.icon, 20 | // ); 21 | 22 | // TODO(terry): IF doesn't work 23 | // const shouldDisplayBadge = useTemplateValue( 24 | // (item: AssetItemType) => !['ETH', 'ERC20'].includes(item.network), 25 | // ); 26 | 27 | return ( 28 | 29 | 30 | 31 | {/* */} 32 | {/* */} 33 | {/* */} 34 | {/* */} 35 | {/* */} 36 | 37 | ); 38 | } 39 | 40 | const styles = StyleSheet.create({ 41 | badgeContainer: { 42 | width: 28, 43 | height: 28, 44 | // backgroundColor: 'black', 45 | borderRadius: 14, 46 | position: 'absolute', 47 | bottom: 10.5, 48 | left: -20, 49 | }, 50 | container: { 51 | elevation: 6, 52 | height: 59, 53 | overflow: 'visible', 54 | paddingTop: 9, 55 | }, 56 | icon: { 57 | borderRadius: 20, 58 | height: 40, 59 | width: 40, 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | 27 | # Version of flipper SDK to use with React Native 28 | FLIPPER_VERSION=0.182.0 29 | 30 | # Use this property to specify which architecture you want to build. 31 | # You can also override it from the CLI using 32 | # ./gradlew -PreactNativeArchitectures=x86_64 33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 34 | 35 | # Use this property to enable support to the new architecture. 36 | # This will allow you to use TurboModules and the Fabric render in 37 | # your application. You should enable this flag either if you want 38 | # to write custom TurboModules/Fabric components OR use libraries that 39 | # are providing them. 40 | newArchEnabled=true 41 | 42 | # Use this property to enable or disable the Hermes JS engine. 43 | # If set to false, you will be using JSC instead. 44 | hermesEnabled=true 45 | -------------------------------------------------------------------------------- /example/src/AssetList/assets.ts: -------------------------------------------------------------------------------- 1 | // this is from trust wallet assets 2 | import tokenList from './tokenList.json'; 3 | 4 | export type AssetItemType = { 5 | id: number; 6 | name: string; 7 | balance: string; 8 | nativeBalance: string; 9 | symbol: string; 10 | network: 'ARBITRUM' | 'ERC20' | 'OPTIMISM' | 'POLYGON' | 'ETH'; 11 | address?: string; 12 | icon?: string; 13 | change?: string; 14 | }; 15 | 16 | function getRandomFloat(min: number, max: number, decimals: number) { 17 | const str = (Math.random() * (max - min) + min).toFixed(decimals); 18 | 19 | return parseFloat(str); 20 | } 21 | 22 | function getChange() { 23 | return Math.random() > 0.5 24 | ? getRandomFloat(-100, 100, 2).toString() 25 | : undefined; 26 | } 27 | 28 | const balanceFormatter = new Intl.NumberFormat('en-US', { 29 | style: 'currency', 30 | currency: 'USD', 31 | }); 32 | const nativeBalanceFormatter = new Intl.NumberFormat('en-US'); 33 | 34 | const ethNativeBalance = getRandomFloat(0, 10000, 8); 35 | 36 | const ethToken: AssetItemType = { 37 | id: 0, 38 | name: 'Etherium', 39 | nativeBalance: nativeBalanceFormatter.format(ethNativeBalance), 40 | balance: balanceFormatter.format( 41 | Math.floor(Math.random() * 100) * ethNativeBalance, 42 | ), 43 | symbol: 'ETH', 44 | network: 'ETH', 45 | change: getChange(), 46 | }; 47 | 48 | const tokens: AssetItemType[] = tokenList 49 | .map((token, index) => { 50 | const nativeBalance = getRandomFloat(0, 100000, 8); 51 | const price = Math.floor(Math.random() * 100); 52 | const change = getChange(); 53 | 54 | return { 55 | id: index + 1, 56 | name: token.name, 57 | address: token.address, 58 | balance: balanceFormatter.format(nativeBalance * price), 59 | nativeBalance: nativeBalanceFormatter.format(nativeBalance), 60 | symbol: token.symbol, 61 | network: token.type as AssetItemType['network'], 62 | icon: token.logoURI, 63 | change, 64 | }; 65 | }) 66 | .sort((a, b) => parseFloat(a.balance) - parseFloat(b.balance)); 67 | 68 | export default [ethToken].concat(...tokens); 69 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ItemProvider.cpp: -------------------------------------------------------------------------------- 1 | #include "ItemProvider.h" 2 | 3 | #include 4 | #include 5 | #include "WishlistJsRuntime.h" 6 | 7 | namespace Wishlist { 8 | 9 | WishItem WorkletItemProvider::provide( 10 | int index, 11 | const std::shared_ptr &prevSn) { 12 | WishItem wishItem; 13 | 14 | auto &rt = WishlistJsRuntime::getInstance().getRuntime(); 15 | 16 | jsi::Function inflateItem = 17 | rt.global() 18 | .getPropertyAsObject(rt, "global") 19 | .getPropertyAsObject(rt, "__wishlistInflatorRegistry") 20 | .getPropertyAsFunction(rt, "inflateItem"); 21 | 22 | jsi::Value returnedValue; 23 | try { 24 | returnedValue = inflateItem.call( 25 | rt, 26 | jsi::String::createFromUtf8(rt, tag), 27 | jsi::Value(index), 28 | cp->prepareProxy(rt), 29 | prevSn ? jsi::Object::createFromHostObject(rt, prevSn) 30 | : jsi::Value::null()); 31 | } catch (std::exception &error) { 32 | di.lock()->getErrorHandler()->reportError(error.what()); 33 | return wishItem; 34 | } 35 | 36 | if (returnedValue.isUndefined()) { 37 | return wishItem; 38 | } 39 | 40 | std::shared_ptr shadowNodeWrapper = 41 | returnedValue.asObject(rt).getHostObject(rt); 42 | 43 | auto sn = shadowNodeWrapper->getShadowNode(); 44 | 45 | auto affected = std::vector(); 46 | this->lc.affectedNodes = &affected; 47 | // better use layoutTree instead of measure (will be persistant) 48 | std::shared_ptr ysn = 49 | std::static_pointer_cast(sn->clone({})); 50 | facebook::react::Size sz = ysn->measure(this->lc, this->lcc); 51 | 52 | wishItem.sn = std::static_pointer_cast(ysn); 53 | wishItem.height = sz.height; 54 | wishItem.index = index; 55 | wishItem.key = shadowNodeWrapper->getKey(); 56 | wishItem.type = shadowNodeWrapper->getType(); 57 | return wishItem; 58 | } 59 | 60 | }; // namespace Wishlist 61 | -------------------------------------------------------------------------------- /cpp/ItemProvider/ShadowNodeCopyMachine.cpp: -------------------------------------------------------------------------------- 1 | #include "ShadowNodeCopyMachine.h" 2 | #include "MGWishlistComponentDescriptor.h" 3 | #include "WishlistJsRuntime.h" 4 | 5 | namespace Wishlist { 6 | 7 | int tag = -2; 8 | 9 | ShadowNode::Shared ShadowNodeCopyMachine::copyShadowSubtree( 10 | const ShadowNode::Shared &sn) { 11 | auto const &cd = sn->getComponentDescriptor(); 12 | 13 | PropsParserContext propsParserContext{ 14 | sn->getSurfaceId(), *cd.getContextContainer().get()}; 15 | 16 | if (tag < -2e9) { 17 | tag = -2; 18 | } 19 | 20 | auto const fragment = 21 | ShadowNodeFamilyFragment{tag -= 2, sn->getSurfaceId(), nullptr}; 22 | auto &rt = WishlistJsRuntime::getInstance().getRuntime(); 23 | auto const eventTarget = 24 | std::make_shared(rt, jsi::Object(rt), tag); 25 | 26 | auto const family = cd.createFamily(fragment, eventTarget); 27 | auto const props = cd.cloneProps( 28 | propsParserContext, 29 | sn->getProps(), 30 | #ifdef ANDROID 31 | sn->getProps()->rawProps 32 | #else 33 | {} 34 | #endif 35 | ); 36 | auto const state = cd.createInitialState(ShadowNodeFragment{props}, family); 37 | 38 | // prevent fabric from clearing EventTarget 39 | auto const *familyH = 40 | reinterpret_cast(family.get()); 41 | familyH->eventEmitter_->setEnabled(true); 42 | 43 | auto shadowNode = cd.createShadowNode( 44 | ShadowNodeFragment{ 45 | /* .props = */ 46 | props, 47 | /* .children = */ ShadowNodeFragment::childrenPlaceholder(), 48 | /* .state = */ state, 49 | }, 50 | family); 51 | 52 | for (const auto &child : sn->getChildren()) { 53 | auto const clonedChild = copyShadowSubtree(child); 54 | cd.appendChild(shadowNode, clonedChild); 55 | } 56 | 57 | return shadowNode; 58 | } 59 | 60 | void ShadowNodeCopyMachine::clearParent(const ShadowNode::Shared &sn) { 61 | auto *family = 62 | reinterpret_cast(&sn->getFamily()); 63 | family->hasParent_ = false; 64 | family->parent_.reset(); 65 | } 66 | 67 | }; // namespace Wishlist 68 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/TemplateContainerViewManager.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.react.bridge.ReadableArray 4 | import com.facebook.react.module.annotations.ReactModule 5 | import com.facebook.react.uimanager.* 6 | import com.facebook.react.uimanager.annotations.ReactProp 7 | import com.facebook.react.viewmanagers.MGTemplateContainerManagerDelegate 8 | import com.facebook.react.viewmanagers.MGTemplateContainerManagerInterface 9 | 10 | @ReactModule(name = TemplateContainerViewManager.REACT_CLASS) 11 | class TemplateContainerViewManager : 12 | ViewGroupManager(), MGTemplateContainerManagerInterface { 13 | companion object { 14 | const val REACT_CLASS = "MGTemplateContainer" 15 | } 16 | 17 | override fun getName() = REACT_CLASS 18 | 19 | override fun createViewInstance(reactContext: ThemedReactContext) = 20 | TemplateContainer(reactContext) 21 | 22 | override fun getDelegate() = MGTemplateContainerManagerDelegate(this) 23 | 24 | override fun updateState( 25 | view: TemplateContainer, 26 | props: ReactStylesDiffMap?, 27 | stateWrapper: StateWrapper? 28 | ): Any? { 29 | view.fabricViewStateManager.setStateWrapper(stateWrapper) 30 | view.updateWishlist() 31 | return null 32 | } 33 | 34 | override fun onAfterUpdateTransaction(view: TemplateContainer) { 35 | super.onAfterUpdateTransaction(view) 36 | view.updateWishlist() 37 | } 38 | 39 | @ReactProp(name = "inflatorId") 40 | override fun setInflatorId(view: TemplateContainer, value: String?) { 41 | view.inflatorId = value 42 | } 43 | 44 | @ReactProp(name = "wishlistId") 45 | override fun setWishlistId(view: TemplateContainer, value: String?) { 46 | view.wishlistId = value 47 | } 48 | 49 | @ReactProp(name = "names") 50 | override fun setNames(view: TemplateContainer, value: ReadableArray?) { 51 | if (value != null) { 52 | val names = ArrayList(value.size()) 53 | for (i in 0 until value.size()) { 54 | names.add(value.getString(i)) 55 | } 56 | view.names = names 57 | } else { 58 | view.names = null 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /android/src/main/jni/Orchestrator.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "MGDIImpl.hpp" 5 | 6 | using namespace facebook; 7 | 8 | namespace Wishlist { 9 | 10 | class Orchestrator : public jni::HybridClass { 11 | public: 12 | Orchestrator( 13 | jni::alias_ref jThis, 14 | const std::string &wishlistId, 15 | int viewportCarerRef); 16 | 17 | static constexpr auto kJavaDescriptor = "Lcom/wishlist/Orchestrator;"; 18 | 19 | static void registerNatives(); 20 | 21 | private: 22 | static jni::local_ref initHybrid( 23 | jni::alias_ref jThis, 24 | std::string wishlistId, 25 | int viewportCarerRef); 26 | 27 | void renderAsync( 28 | float width, 29 | float height, 30 | float initialContentSize, 31 | int originItem, 32 | int templatesRef, 33 | jni::alias_ref> names, 34 | std::string inflatorId); 35 | 36 | void didScrollAsync( 37 | float width, 38 | float height, 39 | float contentOffset, 40 | std::string inflatorId); 41 | 42 | void handleVSync(); 43 | 44 | void didUpdateContentOffset(); 45 | 46 | void scrollToItem(int index); 47 | 48 | void didPushChildren(std::vector items); 49 | 50 | class Adapter final : public MGViewportCarerListener, 51 | public MGVSyncRequester { 52 | public: 53 | Adapter( 54 | std::function onRequestVSync, 55 | std::function items)> didPushChildren); 56 | 57 | private: 58 | void didPushChildren(std::vector newWindow) override; 59 | void requestVSync() override; 60 | 61 | std::function onRequestVSync_; 62 | std::function items)> didPushChildren_; 63 | }; 64 | 65 | friend HybridBase; 66 | 67 | jni::global_ref javaPart_; 68 | bool alreadyRendered_; 69 | std::shared_ptr di_; 70 | std::shared_ptr adapter_; 71 | float width_; 72 | float height_; 73 | std::string inflatorId_; 74 | std::vector items_; 75 | int pendingScrollToItem_; 76 | }; 77 | 78 | } // namespace Wishlist 79 | -------------------------------------------------------------------------------- /example/src/Chat/ChatHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Image, StyleSheet, Text, View } from 'react-native'; 3 | import { TouchableOpacity } from 'react-native-gesture-handler'; 4 | 5 | type HeaderProps = { 6 | isLoading: boolean; 7 | onRefreshPress?: () => void; 8 | }; 9 | 10 | const avatar = require('./assets/margelo_logo.png'); 11 | 12 | export function ChatHeader({ isLoading, onRefreshPress }: HeaderProps) { 13 | return ( 14 | 15 | 16 | 17 | {!isLoading && ( 18 | 19 | )} 20 | 21 | 22 | 23 | {!isLoading && Margelo.io} 24 | 25 | 26 | {onRefreshPress != null && ( 27 | 28 | 32 | 33 | )} 34 | 35 | 36 | ); 37 | } 38 | 39 | const styles = StyleSheet.create({ 40 | container: { 41 | height: 108, 42 | paddingTop: 58, 43 | paddingHorizontal: 19, 44 | flexDirection: 'row', 45 | alignItems: 'center', 46 | justifyContent: 'space-between', 47 | }, 48 | avatar: { 49 | width: 20, 50 | height: 20, 51 | }, 52 | avatarContainer: { 53 | width: 34, 54 | height: 34, 55 | borderRadius: 17, 56 | backgroundColor: 'black', 57 | justifyContent: 'center', 58 | alignItems: 'center', 59 | }, 60 | gray: { 61 | backgroundColor: '#9DA0A8', 62 | }, 63 | center: { 64 | flex: 1, 65 | alignItems: 'center', 66 | }, 67 | left: { 68 | width: 60, 69 | }, 70 | right: { 71 | width: 60, 72 | alignItems: 'flex-end', 73 | }, 74 | title: { 75 | fontSize: 18, 76 | fontWeight: '500', 77 | }, 78 | iconButton: { 79 | padding: 8, 80 | }, 81 | icon: { 82 | width: 24, 83 | height: 24, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/wishlistexample/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.wishlistexample; 2 | 3 | import android.app.Application; 4 | import com.facebook.react.PackageList; 5 | import com.facebook.react.ReactApplication; 6 | import com.facebook.react.ReactNativeHost; 7 | import com.facebook.react.ReactPackage; 8 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; 9 | import com.facebook.react.defaults.DefaultReactNativeHost; 10 | import com.facebook.soloader.SoLoader; 11 | import java.util.List; 12 | 13 | public class MainApplication extends Application implements ReactApplication { 14 | 15 | private final ReactNativeHost mReactNativeHost = 16 | new DefaultReactNativeHost(this) { 17 | @Override 18 | public boolean getUseDeveloperSupport() { 19 | return BuildConfig.DEBUG; 20 | } 21 | 22 | @Override 23 | protected List getPackages() { 24 | @SuppressWarnings("UnnecessaryLocalVariable") 25 | List packages = new PackageList(this).getPackages(); 26 | // Packages that cannot be autolinked yet can be added manually here, for example: 27 | // packages.add(new MyReactNativePackage()); 28 | return packages; 29 | } 30 | 31 | @Override 32 | protected String getJSMainModuleName() { 33 | return "index"; 34 | } 35 | 36 | @Override 37 | protected boolean isNewArchEnabled() { 38 | return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED; 39 | } 40 | 41 | @Override 42 | protected Boolean isHermesEnabled() { 43 | return BuildConfig.IS_HERMES_ENABLED; 44 | } 45 | }; 46 | 47 | @Override 48 | public ReactNativeHost getReactNativeHost() { 49 | return mReactNativeHost; 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | SoLoader.init(this, /* native exopackage */ false); 56 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 57 | // If you opted-in for the New Architecture, we load the native entry point for this app. 58 | DefaultNewArchitectureEntryPoint.load(); 59 | } 60 | ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/ios/WishlistExampleTests/WishlistExampleTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | #import 5 | #import 6 | 7 | #define TIMEOUT_SECONDS 600 8 | #define TEXT_TO_LOOK_FOR @"Welcome to React" 9 | 10 | @interface WishlistExampleTests : XCTestCase 11 | 12 | @end 13 | 14 | @implementation WishlistExampleTests 15 | 16 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL (^)(UIView *view))test 17 | { 18 | if (test(view)) { 19 | return YES; 20 | } 21 | for (UIView *subview in [view subviews]) { 22 | if ([self findSubviewInView:subview matching:test]) { 23 | return YES; 24 | } 25 | } 26 | return NO; 27 | } 28 | 29 | - (void)testRendersWelcomeScreen 30 | { 31 | UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; 32 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 33 | BOOL foundElement = NO; 34 | 35 | __block NSString *redboxError = nil; 36 | #ifdef DEBUG 37 | RCTSetLogFunction( 38 | ^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 39 | if (level >= RCTLogLevelError) { 40 | redboxError = message; 41 | } 42 | }); 43 | #endif 44 | 45 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 46 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 47 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | 49 | foundElement = [self findSubviewInView:vc.view 50 | matching:^BOOL(UIView *view) { 51 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 52 | return YES; 53 | } 54 | return NO; 55 | }]; 56 | } 57 | 58 | #ifdef DEBUG 59 | RCTSetLogFunction(RCTDefaultLogFunction); 60 | #endif 61 | 62 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 63 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 64 | } 65 | 66 | @end 67 | -------------------------------------------------------------------------------- /ios/MGObjCJSIUtils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | using namespace facebook; 6 | 7 | // Copied from RCTTurboModule.mm 8 | 9 | /** 10 | * All static helper functions are ObjC++ specific. 11 | */ 12 | static jsi::Value convertNSNumberToJSIBoolean(jsi::Runtime &runtime, NSNumber *value) 13 | { 14 | return jsi::Value((bool)[value boolValue]); 15 | } 16 | 17 | static jsi::Value convertNSNumberToJSINumber(jsi::Runtime &runtime, NSNumber *value) 18 | { 19 | return jsi::Value([value doubleValue]); 20 | } 21 | 22 | static jsi::String convertNSStringToJSIString(jsi::Runtime &runtime, NSString *value) 23 | { 24 | return jsi::String::createFromUtf8(runtime, [value UTF8String] ?: ""); 25 | } 26 | 27 | static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value); 28 | static jsi::Object convertNSDictionaryToJSIObject(jsi::Runtime &runtime, NSDictionary *value) 29 | { 30 | jsi::Object result = jsi::Object(runtime); 31 | for (NSString *k in value) { 32 | result.setProperty(runtime, [k UTF8String], convertObjCObjectToJSIValue(runtime, value[k])); 33 | } 34 | return result; 35 | } 36 | 37 | static jsi::Array convertNSArrayToJSIArray(jsi::Runtime &runtime, NSArray *value) 38 | { 39 | jsi::Array result = jsi::Array(runtime, value.count); 40 | for (size_t i = 0; i < value.count; i++) { 41 | result.setValueAtIndex(runtime, i, convertObjCObjectToJSIValue(runtime, value[i])); 42 | } 43 | return result; 44 | } 45 | 46 | static jsi::Value convertObjCObjectToJSIValue(jsi::Runtime &runtime, id value) 47 | { 48 | if ([value isKindOfClass:[NSString class]]) { 49 | return convertNSStringToJSIString(runtime, (NSString *)value); 50 | } else if ([value isKindOfClass:[NSNumber class]]) { 51 | if ([value isKindOfClass:[@YES class]]) { 52 | return convertNSNumberToJSIBoolean(runtime, (NSNumber *)value); 53 | } 54 | return convertNSNumberToJSINumber(runtime, (NSNumber *)value); 55 | } else if ([value isKindOfClass:[NSDictionary class]]) { 56 | return convertNSDictionaryToJSIObject(runtime, (NSDictionary *)value); 57 | } else if ([value isKindOfClass:[NSArray class]]) { 58 | return convertNSArrayToJSIArray(runtime, (NSArray *)value); 59 | } else if (value == (id)kCFNull) { 60 | return jsi::Value::null(); 61 | } 62 | return jsi::Value::undefined(); 63 | } 64 | -------------------------------------------------------------------------------- /cpp/MGViewportCarer/MGViewportCarerImpl.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "ItemProvider.h" 9 | #include "MGDI.hpp" 10 | #include "MGViewportCarer.hpp" 11 | 12 | namespace facebook::react { 13 | class MGWishlistShadowNode; 14 | }; 15 | 16 | namespace Wishlist { 17 | 18 | // TODO make this class testable by injecting componentsPool and itemProvider 19 | // or their factories 20 | class MGViewportCarerImpl final : public MGViewportCarer { 21 | public: 22 | MGViewportCarerImpl(); 23 | 24 | void setInitialValues( 25 | const std::shared_ptr &wishListNode, 26 | const LayoutContext &lc); 27 | 28 | void setDI(const std::weak_ptr &_di); 29 | 30 | void setListener(const std::weak_ptr &listener); 31 | 32 | void initialRenderAsync( 33 | MGDims dimensions, 34 | float initialContentSize, 35 | int originItem, 36 | const std::vector> ®isteredViews, 37 | const std::vector &names, 38 | const std::string &inflatorId) override; 39 | 40 | void didScrollAsync( 41 | MGDims dimensions, 42 | float contentOffset, 43 | const std::string &inflatorId) override; 44 | 45 | void didUpdateContentOffset() override; 46 | 47 | private: 48 | void updateWindow(); 49 | 50 | void updateContentOffset(float contentOffset); 51 | 52 | std::shared_ptr getOffseter(float offset); 53 | 54 | void pushChildren(float contentOffset); 55 | 56 | void notifyAboutPushedChildren(); 57 | 58 | void notifyAboutStartReached(); 59 | 60 | void notifyAboutEndReached(); 61 | 62 | float contentOffset_; 63 | float initialContentSize_; 64 | float windowHeight_; 65 | float windowWidth_; 66 | int surfaceId_; 67 | std::string inflatorId_; 68 | std::shared_ptr componentsPool_; 69 | std::shared_ptr itemProvider_; 70 | std::deque window_; 71 | std::shared_ptr wishListNode_; 72 | LayoutContext lc_; 73 | std::weak_ptr di_; 74 | std::string firstItemKeyForStartReached_; 75 | std::string lastItemKeyForEndReached_; 76 | std::weak_ptr listener_; 77 | bool ignoreScrollEvents_; 78 | }; 79 | 80 | }; // namespace Wishlist 81 | -------------------------------------------------------------------------------- /ios/MGTemplateContainerComponent.mm: -------------------------------------------------------------------------------- 1 | #import "MGTemplateContainerComponent.h" 2 | #import 3 | #import "MGTemplateContainerComponentDescriptor.h" 4 | #import "MGTemplateContainerShadowNode.h" 5 | #import "RCTFabricComponentsPlugins.h" 6 | 7 | using namespace facebook::react; 8 | 9 | @implementation MGTemplateContainerComponent { 10 | MGTemplateContainerShadowNode::ConcreteState::Shared _state; 11 | MGWishListComponent *_wishList; 12 | } 13 | 14 | - (instancetype)initWithFrame:(CGRect)frame 15 | { 16 | if (self = [super initWithFrame:frame]) { 17 | static const auto defaultProps = std::make_shared(); 18 | _props = defaultProps; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (void)setWishlist:(MGWishListComponent *)wishList 25 | { 26 | _wishList = wishList; 27 | [self updateWishlist]; 28 | } 29 | 30 | #pragma mark - RCTComponentViewProtocol 31 | 32 | + (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider 33 | { 34 | return facebook::react::concreteComponentDescriptorProvider< 35 | facebook::react::MGTemplateContainerComponentDescriptor>(); 36 | } 37 | 38 | - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps 39 | { 40 | [super updateProps:props oldProps:oldProps]; 41 | [self updateWishlist]; 42 | } 43 | 44 | - (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState 45 | { 46 | auto templateState = std::static_pointer_cast(state); 47 | _state = templateState; 48 | [self updateWishlist]; 49 | } 50 | 51 | - (void)prepareForRecycle 52 | { 53 | [super prepareForRecycle]; 54 | 55 | _wishList = nil; 56 | _state.reset(); 57 | } 58 | 59 | - (void)updateWishlist 60 | { 61 | if (!_wishList || !_state) { 62 | return; 63 | } 64 | auto props = *std::static_pointer_cast(_props); 65 | auto state = std::static_pointer_cast(_state); 66 | [_wishList setWishlistId:props.wishlistId]; 67 | [_wishList setInflatorId:props.inflatorId]; 68 | [_wishList setTemplates:state->getData().getTemplates() withNames:props.names]; 69 | } 70 | 71 | @end 72 | 73 | Class MGTemplateContainerCls(void) 74 | { 75 | return MGTemplateContainerComponent.class; 76 | } 77 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | ENV['RCT_NEW_ARCH_ENABLED'] = '1' 9 | 10 | platform :ios, '13.4' 11 | prepare_react_native_project! 12 | 13 | # If you are using a `react-native-flipper` your iOS build will fail when `NO_FLIPPER=1` is set. 14 | # because `react-native-flipper` depends on (FlipperKit,...) that will be excluded 15 | # 16 | # To fix this you can also exclude `react-native-flipper` using a `react-native.config.js` 17 | # ```js 18 | # module.exports = { 19 | # dependencies: { 20 | # ...(process.env.NO_FLIPPER ? { 'react-native-flipper': { platforms: { ios: null } } } : {}), 21 | # ``` 22 | flipper_config = ENV['NO_FLIPPER'] == "1" ? FlipperConfiguration.disabled : FlipperConfiguration.enabled 23 | 24 | linkage = ENV['USE_FRAMEWORKS'] 25 | if linkage != nil 26 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 27 | use_frameworks! :linkage => linkage.to_sym 28 | end 29 | 30 | target 'WishlistExample' do 31 | config = use_native_modules! 32 | 33 | # Flags change depending on the env values. 34 | flags = get_default_flags() 35 | 36 | use_react_native!( 37 | :path => config[:reactNativePath], 38 | # Hermes is now enabled by default. Disable by setting this flag to false. 39 | # Upcoming versions of React Native may rely on get_default_flags(), but 40 | # we make it explicit here to aid in the React Native upgrade process. 41 | :hermes_enabled => flags[:hermes_enabled], 42 | :fabric_enabled => flags[:fabric_enabled], 43 | # Enables Flipper. 44 | # 45 | # Note that if you have use_frameworks! enabled, Flipper will not work and 46 | # you should disable the next line. 47 | :flipper_configuration => flipper_config, 48 | # An absolute path to your application root. 49 | :app_path => "#{Pod::Config.instance.installation_root}/.." 50 | ) 51 | 52 | target 'WishlistExampleTests' do 53 | inherit! :complete 54 | # Pods for testing 55 | end 56 | 57 | post_install do |installer| 58 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 59 | react_native_post_install( 60 | installer, 61 | config[:reactNativePath], 62 | :mac_catalyst_enabled => false 63 | ) 64 | __apply_Xcode_12_5_M1_post_install_workaround(installer) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /android/src/main/java/com/wishlist/WishlistViewManager.kt: -------------------------------------------------------------------------------- 1 | package com.wishlist 2 | 3 | import com.facebook.react.bridge.ReadableArray 4 | import com.facebook.react.common.MapBuilder 5 | import com.facebook.react.module.annotations.ReactModule 6 | import com.facebook.react.uimanager.ReactStylesDiffMap 7 | import com.facebook.react.uimanager.StateWrapper 8 | import com.facebook.react.uimanager.ThemedReactContext 9 | import com.facebook.react.uimanager.ViewGroupManager 10 | import com.facebook.react.uimanager.annotations.ReactProp 11 | import com.facebook.react.viewmanagers.MGWishlistManagerDelegate 12 | import com.facebook.react.viewmanagers.MGWishlistManagerInterface 13 | 14 | @ReactModule(name = WishlistViewManager.REACT_CLASS) 15 | class WishlistViewManager : ViewGroupManager(), MGWishlistManagerInterface { 16 | companion object { 17 | const val REACT_CLASS = "MGWishlist" 18 | } 19 | 20 | private val mDelegate = MGWishlistManagerDelegate(this) 21 | 22 | override fun getName() = REACT_CLASS 23 | 24 | override fun createViewInstance(reactContext: ThemedReactContext) = Wishlist(reactContext) 25 | 26 | override fun getDelegate() = mDelegate 27 | 28 | override fun updateState( 29 | view: Wishlist, 30 | props: ReactStylesDiffMap?, 31 | stateWrapper: StateWrapper? 32 | ): Any? { 33 | view.fabricViewStateManager.setStateWrapper(stateWrapper) 34 | val stateData = stateWrapper?.stateData 35 | if (stateData != null && stateData.hasKey("contentOffset")) { 36 | view.scrollToOffsetForContentChange(stateData.getDouble("contentOffset").toFloat()) 37 | } 38 | return null 39 | } 40 | 41 | @ReactProp(name = "inflatorId") 42 | override fun setInflatorId(view: Wishlist, value: String?) { 43 | view.inflatorId = value 44 | } 45 | 46 | @ReactProp(name = "initialIndex") 47 | override fun setInitialIndex(view: Wishlist, value: Int) { 48 | view.initialIndex = value 49 | } 50 | 51 | override fun scrollToItem(view: Wishlist, index: Int, animated: Boolean) { 52 | view.scrollToItem(index, animated) 53 | } 54 | 55 | override fun receiveCommand(root: Wishlist, commandId: String?, args: ReadableArray?) { 56 | mDelegate.receiveCommand(root, commandId, args) 57 | } 58 | 59 | override fun getExportedCustomDirectEventTypeConstants(): MutableMap? { 60 | return MapBuilder.builder() 61 | .put("topStartReached", MapBuilder.of("registrationName", "onStartReached")) 62 | .put("topEndReached", MapBuilder.of("registrationName", "onEndReached")) 63 | .build() 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /example/src/AssetList/AssetListSeparator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | import { Button } from './Button'; 4 | import { useTemplateValue, Wishlist } from 'react-native-wishlist'; 5 | import type { AssetListSeparatorWithState } from './AssetListExample'; 6 | 7 | type AssetListSeparatorProps = { 8 | onExpand: () => void; 9 | onEdit: () => void; 10 | }; 11 | 12 | export function AssetListSeparator({ 13 | onExpand, 14 | onEdit, 15 | }: AssetListSeparatorProps) { 16 | const isEditing = useTemplateValue( 17 | (item: AssetListSeparatorWithState) => item.isEditing, 18 | ); 19 | const expandButtonText = useTemplateValue( 20 | (item: AssetListSeparatorWithState) => 21 | item.isExpanded ? 'Less ↑' : 'More ↓', 22 | ); 23 | const isExpanded = useTemplateValue( 24 | (item: AssetListSeparatorWithState) => item.isExpanded, 25 | ); 26 | const editButtonText = useTemplateValue((item: AssetListSeparatorWithState) => 27 | item.isEditing ? 'Done' : 'Edit', 28 | ); 29 | 30 | const onPin = () => { 31 | 'worklet'; 32 | }; 33 | const onHide = () => { 34 | 'worklet'; 35 | }; 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 |