diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index 0e16f56a3..a42101bf4 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -13,6 +13,7 @@ import com.rnmapbox.rnmbx.components.annotation.RNMBXMarkerViewManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationManager import com.rnmapbox.rnmbx.components.annotation.RNMBXPointAnnotationModule import com.rnmapbox.rnmbx.components.camera.RNMBXCameraManager +import com.rnmapbox.rnmbx.components.camera.RNMBXCameraModule import com.rnmapbox.rnmbx.components.camera.RNMBXViewport import com.rnmapbox.rnmbx.components.camera.RNMBXViewportManager import com.rnmapbox.rnmbx.components.camera.RNMBXViewportModule @@ -96,6 +97,7 @@ class RNMBXPackage : TurboReactPackage() { RNMBXSnapshotModule.REACT_CLASS -> return RNMBXSnapshotModule(reactApplicationContext) RNMBXLogging.REACT_CLASS -> return RNMBXLogging(reactApplicationContext) NativeMapViewModule.NAME -> return NativeMapViewModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) + RNMBXCameraModule.NAME -> return RNMBXCameraModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) RNMBXViewportModule.NAME -> return RNMBXViewportModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) RNMBXShapeSourceModule.NAME -> return RNMBXShapeSourceModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) RNMBXImageModule.NAME -> return RNMBXImageModule(reactApplicationContext, getViewTagResolver(reactApplicationContext, s)) @@ -114,8 +116,8 @@ class RNMBXPackage : TurboReactPackage() { val managers: MutableList> = ArrayList() // components - managers.add(RNMBXCameraManager(reactApplicationContext)) - managers.add(RNMBXViewportManager(reactApplicationContext)) + managers.add(RNMBXCameraManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXCameraManager"))) + managers.add(RNMBXViewportManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXViewportManager"))) managers.add(RNMBXMapViewManager(reactApplicationContext, getViewTagResolver(reactApplicationContext, "RNMBXMapViewManager"))) managers.add(RNMBXStyleImportManager(reactApplicationContext)) managers.add(RNMBXModelsManager(reactApplicationContext)) @@ -245,6 +247,15 @@ class RNMBXPackage : TurboReactPackage() { false, // isCxxModule isTurboModule // isTurboModule ) + moduleInfos[RNMBXCameraModule.NAME] = ReactModuleInfo( + RNMBXCameraModule.NAME, + RNMBXCameraModule.NAME, + false, // canOverrideExistingModule + false, // needsEagerInit + false, // hasConstants + false, // isCxxModule + isTurboModule // isTurboModule + ) moduleInfos[RNMBXShapeSourceModule.NAME] = ReactModuleInfo( RNMBXShapeSourceModule.NAME, RNMBXShapeSourceModule.NAME, diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt index 57aea256a..144053411 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/CameraStop.kt @@ -31,8 +31,6 @@ class CameraStop { private var mDuration = 2000 private var mCallback: Animator.AnimatorListener? = null - var ts: Int? = null - fun setBearing(bearing: Double) { mBearing = bearing } @@ -156,10 +154,6 @@ class CameraStop { ): CameraStop { val stop = CameraStop() - if (readableMap.hasKey("__updateTS")) { - stop.ts = readableMap.getInt("__updateTS") - } - if (readableMap.hasKey("pitch")) { stop.setTilt(readableMap.getDouble("pitch")) } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt index 0d876a7e8..aaae75f24 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt @@ -3,6 +3,7 @@ package com.rnmapbox.rnmbx.components.camera import android.animation.Animator import android.content.Context import android.location.Location +import com.facebook.react.bridge.Dynamic import com.facebook.react.bridge.ReadableMap import com.mapbox.maps.plugin.gestures.gestures import com.rnmapbox.rnmbx.location.LocationManager.Companion.getInstance @@ -15,6 +16,7 @@ import com.mapbox.maps.plugin.locationcomponent.OnIndicatorBearingChangedListene import com.mapbox.maps.plugin.locationcomponent.OnIndicatorPositionChangedListener import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeMap +import com.facebook.react.uimanager.annotations.ReactProp import com.mapbox.maps.* import com.mapbox.maps.plugin.locationcomponent.location import com.mapbox.maps.plugin.viewport.ViewportStatus @@ -113,15 +115,18 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame } } fun setStop(stop: CameraStop) { - if ((stop.ts != mCameraStop?.ts) || (mCameraStop == null)) { - mCameraStop = stop - stop.setCallback(mCameraCallback) - if (mMapView != null) { - stop.let { updateCamera(it) } - } + mCameraStop = stop + stop.setCallback(mCameraCallback) + if (mMapView != null) { + stop.let { updateCamera(it) } } } + fun updateCameraStop(map: ReadableMap) { + val stop = CameraStop.fromReadableMap(mContext, map, null) + setStop(stop) + } + fun setDefaultStop(stop: CameraStop?) { mDefaultStop = stop } diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt index 480c8c215..305c5cef4 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt @@ -9,11 +9,12 @@ import com.mapbox.geojson.FeatureCollection import com.rnmapbox.rnmbx.components.AbstractEventEmitter import com.rnmapbox.rnmbx.components.camera.CameraStop.Companion.fromReadableMap import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toLatLngBounds +import com.rnmapbox.rnmbx.utils.ViewTagResolver import com.rnmapbox.rnmbx.utils.extensions.asBooleanOrNull import com.rnmapbox.rnmbx.utils.extensions.asDoubleOrNull import com.rnmapbox.rnmbx.utils.extensions.asStringOrNull -class RNMBXCameraManager(private val mContext: ReactApplicationContext) : +class RNMBXCameraManager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) : AbstractEventEmitter( mContext ), RNMBXCameraManagerInterface { diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt new file mode 100644 index 000000000..a07e67f1c --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraModule.kt @@ -0,0 +1,51 @@ +package com.rnmapbox.rnmbx.components.camera + +import com.facebook.react.bridge.Callback +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeMap +import com.rnmapbox.rnmbx.NativeRNMBXCameraModuleSpec +import com.rnmapbox.rnmbx.components.mapview.CommandResponse +import com.rnmapbox.rnmbx.utils.ViewTagResolver + + +class RNMBXCameraModule(context: ReactApplicationContext, val viewTagResolver: ViewTagResolver) : NativeRNMBXCameraModuleSpec(context) { + private fun withViewportOnUIThread( + viewRef: Double?, + reject: Promise, + fn: (RNMBXCamera) -> Unit + ) { + if (viewRef == null) { + reject.reject(Exception("viewRef is null")) + } else { + viewTagResolver.withViewResolved(viewRef.toInt(), reject, fn) + } + } + + private fun createCommandResponse(promise: Promise): CommandResponse = object : CommandResponse { + override fun success(builder: (WritableMap) -> Unit) { + val payload: WritableMap = WritableNativeMap() + builder(payload) + + promise.resolve(payload) + } + + override fun error(message: String) { + promise.reject(Exception(message)) + } + } + + companion object { + const val NAME = "RNMBXCameraModule" + } + + override fun updateCameraStop(viewRef: Double?, stop: ReadableMap, promise: Promise) { + withViewportOnUIThread(viewRef, promise) { + it.updateCameraStop(stop) + promise.resolve(null) + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXVIewportManager.kt b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXVIewportManager.kt index 5dd5b22a7..4460eab7a 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXVIewportManager.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXVIewportManager.kt @@ -12,8 +12,9 @@ import com.facebook.react.viewmanagers.RNMBXViewportManagerInterface import com.rnmapbox.rnmbx.components.AbstractEventEmitter import com.rnmapbox.rnmbx.events.constants.EventKeys import com.rnmapbox.rnmbx.events.constants.eventMapOf +import com.rnmapbox.rnmbx.utils.ViewTagResolver -class RNMBXViewportManager(private val mContext: ReactApplicationContext) : AbstractEventEmitter( +class RNMBXViewportManager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) : AbstractEventEmitter( mContext ), RNMBXViewportManagerInterface { diff --git a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java new file mode 100644 index 000000000..96ba3d653 --- /dev/null +++ b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXCameraModuleSpec.java @@ -0,0 +1,41 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJavaSpec.js + * + * @nolint + */ + +package com.rnmapbox.rnmbx; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class NativeRNMBXCameraModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { + public static final String NAME = "RNMBXCameraModule"; + + public NativeRNMBXCameraModuleSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + + @ReactMethod + @DoNotStrip + public abstract void updateCameraStop(@Nullable Double viewRef, ReadableMap stops, Promise promise); +} diff --git a/ios/RNMBX/RNMBXCamera.swift b/ios/RNMBX/RNMBXCamera.swift index 40ec5de31..32e187580 100644 --- a/ios/RNMBX/RNMBXCamera.swift +++ b/ios/RNMBX/RNMBXCamera.swift @@ -237,6 +237,10 @@ open class RNMBXCamera : RNMBXMapComponentBase { map.viewport.idle() } + @objc public func updateCameraStop(_ stop: [String: Any]) { + self.stop = stop + } + func _toCoordinateBounds(_ bounds: FeatureCollection) throws -> CoordinateBounds { guard bounds.features.count == 2 else { throw RNMBXError.paramError("Expected two Points in FeatureColletion") diff --git a/ios/RNMBX/RNMBXCameraModule.h b/ios/RNMBX/RNMBXCameraModule.h new file mode 100644 index 000000000..293ea51f1 --- /dev/null +++ b/ios/RNMBX/RNMBXCameraModule.h @@ -0,0 +1,18 @@ +#import +#import + +#ifdef RCT_NEW_ARCH_ENABLED +#import "rnmapbox_maps_specs.h" +#else +#import +#endif + +@interface RNMBXCameraModule : NSObject +#ifdef RCT_NEW_ARCH_ENABLED + +#else + +#endif + +@end + diff --git a/ios/RNMBX/RNMBXCameraModule.mm b/ios/RNMBX/RNMBXCameraModule.mm new file mode 100644 index 000000000..81fc53b5c --- /dev/null +++ b/ios/RNMBX/RNMBXCameraModule.mm @@ -0,0 +1,67 @@ +#import +#import +#import + +#import "RNMBXCameraModule.h" +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNMBXCameraComponentView.h" +#endif // RCT_NEW_ARCH_ENABLED + +#import "rnmapbox_maps-Swift.pre.h" + +@implementation RNMBXCameraModule + +RCT_EXPORT_MODULE(); + +#ifdef RCT_NEW_ARCH_ENABLED +@synthesize viewRegistry_DEPRECATED = _viewRegistry_DEPRECATED; +#endif // RCT_NEW_ARCH_ENABLED +@synthesize bridge = _bridge; + +- (dispatch_queue_t)methodQueue +{ + // It seems that due to how UIBlocks work with uiManager, we need to call the methods there + // for the blocks to be dispatched before the batch is completed + return RCTGetUIManagerQueue(); +} + +// Thanks to this guard, we won't compile this code when we build for the old architecture. +#ifdef RCT_NEW_ARCH_ENABLED +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params +{ + return std::make_shared(params); +} +#endif // RCT_NEW_ARCH_ENABLED + +- (void)withCamera:(nonnull NSNumber*)viewRef block:(void (^)(RNMBXCamera *))block reject:(RCTPromiseRejectBlock)reject methodName:(NSString *)methodName +{ +#ifdef RCT_NEW_ARCH_ENABLED + [self.viewRegistry_DEPRECATED addUIBlock:^(RCTViewRegistry *viewRegistry) { + RNMBXCameraComponentView *componentView = [self.viewRegistry_DEPRECATED viewForReactTag:viewRef]; + RNMBXCamera *view = componentView.contentView; + +#else + [self.bridge.uiManager + addUIBlock:^(RCTUIManager *uiManager, NSDictionary *viewRegistry) { + RNMBXCamera *view = [uiManager viewForReactTag:viewRef]; +#endif // RCT_NEW_ARCH_ENABLED + if (view != nil) { + block(view); + } else { + reject(methodName, [NSString stringWithFormat:@"Unknown reactTag: %@", viewRef], nil); + } + }]; +} + +RCT_EXPORT_METHOD(updateCameraStop:(nonnull NSNumber *)viewRef + stop: (NSDictionary*)stop + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) +{ + [self withCamera:viewRef block:^(RNMBXCamera *view) { + [view updateCameraStop: stop]; + } reject:reject methodName:@"someMethod"]; +} + +@end diff --git a/setup-jest.js b/setup-jest.js index e168c5efd..78d14f0d6 100644 --- a/setup-jest.js +++ b/setup-jest.js @@ -186,6 +186,10 @@ NativeModules.RNMBXViewportModule = { getState: jest.fn(), }; +NativeModules.RNMBXCameraModule = { + updateCameraStop: jest.fn(), +}; + NativeModules.RNMBXTileStoreModule = { setOptions: jest.fn(), shared: jest.fn(), diff --git a/src/components/Camera.tsx b/src/components/Camera.tsx index b09ce24a1..cc6c7be92 100644 --- a/src/components/Camera.tsx +++ b/src/components/Camera.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, memo, useCallback, + useEffect, useImperativeHandle, useMemo, useRef, @@ -13,6 +14,8 @@ import { type Position } from '../types/Position'; import { makeLatLngBounds, makePoint } from '../utils/geoUtils'; import { type NativeRefType } from '../utils/nativeRef'; import NativeCameraView from '../specs/RNMBXCameraNativeComponent'; +import RNMBXCameraModule from '../specs/NativeRNMBXCameraModule'; +import { NativeCommands, type NativeArg } from '../utils/NativeCommands'; const NativeModule = NativeModules.RNMBXModule; @@ -250,6 +253,15 @@ export const Camera = memo( null, ) as NativeRefType; + const commands = useMemo(() => new NativeCommands(RNMBXCameraModule), []); + + useEffect(() => { + if (nativeCamera.current) { + commands.setNativeRef(nativeCamera.current); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [commands, nativeCamera.current]); + const buildNativeStop = useCallback( ( stop: CameraStop, @@ -380,8 +392,6 @@ export const Camera = memo( return; } - lastTS += 1; - if (!config.type) // @ts-expect-error The compiler doesn't understand that the `config` union type is guaranteed // to be an object type. @@ -399,22 +409,26 @@ export const Camera = memo( if (_nativeStop) { _nativeStops = [..._nativeStops, _nativeStop]; } - nativeCamera.current?.setNativeProps({ - stop: { stops: _nativeStops, __updateTS: lastTS }, - }); + + commands.call('updateCameraStop', [ + { + stops: _nativeStops, + } as unknown as NativeArg[], + ]); } } else if (config.type === 'CameraStop') { const _nativeStop = buildNativeStop(config); if (_nativeStop) { - nativeCamera.current?.setNativeProps({ - stop: { __updateTS: lastTS, ..._nativeStop }, - }); + commands.call('updateCameraStop', [ + _nativeStop as unknown as NativeArg, + ]); } } }; const setCamera = useCallback(_setCamera, [ allowUpdates, buildNativeStop, + commands, ]); const _fitBounds: CameraRef['fitBounds'] = ( @@ -594,8 +608,6 @@ export const Camera = memo( ), ); -let lastTS = 0; - const RNMBXCamera = NativeCameraView; export type Camera = CameraRef; diff --git a/src/components/Viewport.tsx b/src/components/Viewport.tsx index ee0102197..f3f612484 100644 --- a/src/components/Viewport.tsx +++ b/src/components/Viewport.tsx @@ -7,18 +7,14 @@ import React, { useMemo, useRef, } from 'react'; -import { - NodeHandle, - findNodeHandle, - type NativeMethods, - type NativeSyntheticEvent, -} from 'react-native'; +import { type NativeMethods, type NativeSyntheticEvent } from 'react-native'; import NativeViewport, { type NativeViewportReal, type OnStatusChangedEventTypeReal, } from '../specs/RNMBXViewportNativeComponent'; import RNMBXViewportModule from '../specs/NativeRNMBXViewportModule'; +import { NativeCommands } from '../utils/NativeCommands'; type FollowPuckOptions = { /** @@ -240,91 +236,3 @@ type NativeProps = Omit & { type RNMBXViewportRefType = Component & Readonly; const RNMBXViewport = NativeViewport as NativeViewportReal; - -export type NativeArg = - | string - | number - | boolean - | null - | { [k: string]: NativeArg } - | NativeArg[] - // eslint-disable-next-line @typescript-eslint/ban-types - | Function - | GeoJSON.Geometry - | undefined; - -type FunctionKeys = keyof { - // eslint-disable-next-line @typescript-eslint/ban-types - [K in keyof T as T[K] extends Function ? K : never]: T[K]; -}; - -type RefType = React.Component; - -class NativeCommands { - module: Spec; - - preRefMethodQueue: Array<{ - method: { name: FunctionKeys; args: NativeArg[] }; - resolver: (value: unknown) => void; - }>; - - nativeRef: RefType | undefined; - - constructor(module: Spec) { - this.module = module; - this.preRefMethodQueue = []; - } - - async setNativeRef(nativeRef: RefType) { - if (nativeRef) { - this.nativeRef = nativeRef; - while (this.preRefMethodQueue.length > 0) { - const item = this.preRefMethodQueue.pop(); - - if (item && item.method && item.resolver) { - const res = await this._call( - item.method.name, - nativeRef, - item.method.args, - ); - item.resolver(res); - } - } - } - } - - call(name: FunctionKeys, args: NativeArg[]): Promise { - if (this.nativeRef) { - return this._call(name, this.nativeRef, args); - } else { - return new Promise((resolve) => { - this.preRefMethodQueue.push({ - method: { name, args }, - resolver: resolve as (args: unknown) => void, - }); - }); - } - } - - _call( - name: FunctionKeys, - nativeRef: RefType, - args: NativeArg[], - ): Promise { - const handle = findNodeHandle(nativeRef); - if (handle) { - return ( - this.module[name] as ( - arg0: NodeHandle, - ...args: NativeArg[] - ) => Promise - )(handle, ...args); - } else { - throw new Error( - `Could not find handle for native ref ${module} when trying to invoke ${String( - name, - )}`, - ); - } - } -} diff --git a/src/specs/NativeRNMBXCameraModule.ts b/src/specs/NativeRNMBXCameraModule.ts new file mode 100644 index 000000000..485a1ad1b --- /dev/null +++ b/src/specs/NativeRNMBXCameraModule.ts @@ -0,0 +1,36 @@ +import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; +import { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; +import { TurboModuleRegistry } from 'react-native'; + +type ViewRef = Int32 | null; + +interface NativeCameraStop { + centerCoordinate?: string; + bounds?: string; + heading?: number; + pitch?: number; + zoom?: number; + paddingLeft?: number; + paddingRight?: number; + paddingTop?: number; + paddingBottom?: number; + duration?: number; + mode?: NativeAnimationMode; +} + +type Stop = + | { + stops: NativeCameraStop[]; + } + | NativeCameraStop; + +type NativeAnimationMode = 'flight' | 'ease' | 'linear' | 'none' | 'move'; + +// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-unused-vars +type ObjectOr = Object; + +export interface Spec extends TurboModule { + updateCameraStop(viewRef: ViewRef, stops: ObjectOr): Promise; +} + +export default TurboModuleRegistry.getEnforcing('RNMBXCameraModule'); diff --git a/src/utils/NativeCommands.ts b/src/utils/NativeCommands.ts new file mode 100644 index 000000000..4f926b5f3 --- /dev/null +++ b/src/utils/NativeCommands.ts @@ -0,0 +1,89 @@ +import { NodeHandle, findNodeHandle } from 'react-native'; + +export type NativeArg = + | string + | number + | boolean + | null + | { [k: string]: NativeArg } + | NativeArg[] + // eslint-disable-next-line @typescript-eslint/ban-types + | Function + | GeoJSON.Geometry + | undefined; + +type FunctionKeys = keyof { + // eslint-disable-next-line @typescript-eslint/ban-types + [K in keyof T as T[K] extends Function ? K : never]: T[K]; +}; + +type RefType = React.Component; + +export class NativeCommands { + module: Spec; + + preRefMethodQueue: Array<{ + method: { name: FunctionKeys; args: NativeArg[] }; + resolver: (value: unknown) => void; + }>; + + nativeRef: RefType | undefined; + + constructor(module: Spec) { + this.module = module; + this.preRefMethodQueue = []; + } + + async setNativeRef(nativeRef: RefType) { + if (nativeRef) { + this.nativeRef = nativeRef; + while (this.preRefMethodQueue.length > 0) { + const item = this.preRefMethodQueue.pop(); + + if (item && item.method && item.resolver) { + const res = await this._call( + item.method.name, + nativeRef, + item.method.args, + ); + item.resolver(res); + } + } + } + } + + call(name: FunctionKeys, args: NativeArg[]): Promise { + if (this.nativeRef) { + return this._call(name, this.nativeRef, args); + } else { + return new Promise((resolve) => { + this.preRefMethodQueue.push({ + method: { name, args }, + resolver: resolve as (args: unknown) => void, + }); + }); + } + } + + _call( + name: FunctionKeys, + nativeRef: RefType, + args: NativeArg[], + ): Promise { + const handle = findNodeHandle(nativeRef); + if (handle) { + return ( + this.module[name] as ( + arg0: NodeHandle, + ...args: NativeArg[] + ) => Promise + )(handle, ...args); + } else { + throw new Error( + `Could not find handle for native ref ${module} when trying to invoke ${String( + name, + )}`, + ); + } + } +}