@granite-js/micro-frontend 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GraniteMicroFrontendRuntime.podspec +40 -0
- package/LICENSE +202 -0
- package/LICENSE.react-native-teleport +21 -0
- package/NOTICE +17 -0
- package/README.md +508 -0
- package/android/CMakeLists.txt +34 -0
- package/android/build.gradle +74 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/cpp/BundleEvaluator.cpp +64 -0
- package/android/src/main/cpp/BundleEvaluator.h +20 -0
- package/android/src/main/cpp/FileReader.cpp +92 -0
- package/android/src/main/cpp/FileReader.h +27 -0
- package/android/src/main/cpp/onLoad.cpp +9 -0
- package/android/src/main/java/com/teleport/extensions/Float.kt +12 -0
- package/android/src/main/java/com/teleport/extensions/View.kt +10 -0
- package/android/src/main/java/com/teleport/global/PortalRegistry.kt +117 -0
- package/android/src/main/java/com/teleport/host/PortalHostView.kt +97 -0
- package/android/src/main/java/com/teleport/host/PortalHostViewManager.kt +41 -0
- package/android/src/main/java/com/teleport/host/PortalReactRootView.kt +116 -0
- package/android/src/main/java/com/teleport/managers/TeleportViewManager.kt +27 -0
- package/android/src/main/java/com/teleport/portal/PortalLayoutStateController.kt +107 -0
- package/android/src/main/java/com/teleport/portal/PortalView.kt +309 -0
- package/android/src/main/java/com/teleport/portal/PortalViewManager.kt +51 -0
- package/android/src/main/java/run/granite/microfrontend/ActivitySessionBinding.kt +87 -0
- package/android/src/main/java/run/granite/microfrontend/BundleEvaluator.kt +46 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendEvent.kt +62 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouter.kt +186 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeHost.kt +42 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeModule.kt +56 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendRuntimePackage.kt +35 -0
- package/android/src/main/java/run/granite/microfrontend/GraniteMicroFrontendSessionStore.kt +116 -0
- package/android/src/main/jni/CMakeLists.txt +64 -0
- package/android/src/test/cpp/CMakeLists.txt +35 -0
- package/android/src/test/cpp/FileReaderTest.cpp +44 -0
- package/android/src/test/java/com/teleport/host/PortalReactRootViewContractTest.kt +76 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendRuntimeEventRouterTest.kt +216 -0
- package/android/src/test/java/run/granite/microfrontend/GraniteMicroFrontendSessionStoreTest.kt +190 -0
- package/android/test-cpp.sh +12 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/ComponentDescriptors.h +24 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.cpp +31 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/PortalShadowRegistry.h +29 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewComponentDescriptor.h +22 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewShadowNode.h +43 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalHostViewState.h +21 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewComponentDescriptor.h +54 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.cpp +7 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewShadowNode.h +51 -0
- package/common/cpp/react/renderer/components/GraniteMicroFrontendRuntimeSpec/RNTPortalViewState.h +43 -0
- package/dist/module/createMicroFrontendRuntime.js +18 -0
- package/dist/module/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/createRoute.js +43 -0
- package/dist/module/createRoute.js.map +1 -0
- package/dist/module/host/PendingHostComponent.js +70 -0
- package/dist/module/host/PendingHostComponent.js.map +1 -0
- package/dist/module/host/index.js +6 -0
- package/dist/module/host/index.js.map +1 -0
- package/dist/module/host/pendingHostComponentStore.js +164 -0
- package/dist/module/host/pendingHostComponentStore.js.map +1 -0
- package/dist/module/host/resolveParams.js +60 -0
- package/dist/module/host/resolveParams.js.map +1 -0
- package/dist/module/host/routeMatcher.js +92 -0
- package/dist/module/host/routeMatcher.js.map +1 -0
- package/dist/module/host/types.js +4 -0
- package/dist/module/host/types.js.map +1 -0
- package/dist/module/index.js +12 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/package.json +1 -0
- package/dist/module/plugin/index.js +4 -0
- package/dist/module/plugin/index.js.map +1 -0
- package/dist/module/plugin/intoShared.js +32 -0
- package/dist/module/plugin/intoShared.js.map +1 -0
- package/dist/module/plugin/microFrontendPlugin.js +58 -0
- package/dist/module/plugin/microFrontendPlugin.js.map +1 -0
- package/dist/module/plugin/prelude.js +23 -0
- package/dist/module/plugin/prelude.js.map +1 -0
- package/dist/module/plugin/resolver.js +25 -0
- package/dist/module/plugin/resolver.js.map +1 -0
- package/dist/module/plugin/types.js +4 -0
- package/dist/module/plugin/types.js.map +1 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js +79 -0
- package/dist/module/runtime/createMicroFrontendRuntime.js.map +1 -0
- package/dist/module/runtime/errors.js +63 -0
- package/dist/module/runtime/errors.js.map +1 -0
- package/dist/module/runtime/parseAppRequest.js +16 -0
- package/dist/module/runtime/parseAppRequest.js.map +1 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js +54 -0
- package/dist/module/runtime/parseNativeRuntimeEvent.js.map +1 -0
- package/dist/module/runtime/registry.js +87 -0
- package/dist/module/runtime/registry.js.map +1 -0
- package/dist/module/session/MicroFrontendSessionContext.js +32 -0
- package/dist/module/session/MicroFrontendSessionContext.js.map +1 -0
- package/dist/module/session/useMicroFrontendSessions.js +47 -0
- package/dist/module/session/useMicroFrontendSessions.js.map +1 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js +21 -0
- package/dist/module/specs/NativeGraniteMicroFrontendRuntime.js.map +1 -0
- package/dist/module/specs/PortalHostViewNativeComponent.ts +9 -0
- package/dist/module/specs/PortalViewNativeComponent.ts +9 -0
- package/dist/module/types.js +2 -0
- package/dist/module/types.js.map +1 -0
- package/dist/typescript/createMicroFrontendRuntime.d.ts +6 -0
- package/dist/typescript/createRoute.d.ts +23 -0
- package/dist/typescript/host/PendingHostComponent.d.ts +14 -0
- package/dist/typescript/host/index.d.ts +5 -0
- package/dist/typescript/host/pendingHostComponentStore.d.ts +11 -0
- package/dist/typescript/host/resolveParams.d.ts +10 -0
- package/dist/typescript/host/routeMatcher.d.ts +7 -0
- package/dist/typescript/host/types.d.ts +32 -0
- package/dist/typescript/index.d.ts +16 -0
- package/dist/typescript/plugin/index.d.ts +2 -0
- package/dist/typescript/plugin/intoShared.d.ts +3 -0
- package/dist/typescript/plugin/microFrontendPlugin.d.ts +3 -0
- package/dist/typescript/plugin/prelude.d.ts +6 -0
- package/dist/typescript/plugin/resolver.d.ts +3 -0
- package/dist/typescript/plugin/types.d.ts +5 -0
- package/dist/typescript/runtime/createMicroFrontendRuntime.d.ts +31 -0
- package/dist/typescript/runtime/errors.d.ts +35 -0
- package/dist/typescript/runtime/parseAppRequest.d.ts +6 -0
- package/dist/typescript/runtime/parseNativeRuntimeEvent.d.ts +3 -0
- package/dist/typescript/runtime/registry.d.ts +35 -0
- package/dist/typescript/session/MicroFrontendSessionContext.d.ts +13 -0
- package/dist/typescript/session/useMicroFrontendSessions.d.ts +8 -0
- package/dist/typescript/specs/NativeGraniteMicroFrontendRuntime.d.ts +21 -0
- package/dist/typescript/specs/PortalHostViewNativeComponent.d.ts +6 -0
- package/dist/typescript/specs/PortalViewNativeComponent.d.ts +6 -0
- package/dist/typescript/types.d.ts +49 -0
- package/ios/GraniteMicroFrontendRuntimeHost+Internal.h +18 -0
- package/ios/GraniteMicroFrontendRuntimeHost.h +34 -0
- package/ios/GraniteMicroFrontendRuntimeHost.mm +485 -0
- package/ios/GraniteMicroFrontendRuntimeModule.h +6 -0
- package/ios/GraniteMicroFrontendRuntimeModule.mm +138 -0
- package/ios/PortalHostContainerView.h +56 -0
- package/ios/PortalHostContainerView.mm +133 -0
- package/ios/PortalHostView.h +39 -0
- package/ios/PortalHostView.mm +153 -0
- package/ios/PortalRegistry.h +38 -0
- package/ios/PortalRegistry.mm +228 -0
- package/ios/PortalView.h +32 -0
- package/ios/PortalView.mm +426 -0
- package/package.json +113 -0
- package/react-native.config.js +13 -0
- package/src/createMicroFrontendRuntime.ts +22 -0
- package/src/createRoute.ts +84 -0
- package/src/host/PendingHostComponent.tsx +92 -0
- package/src/host/index.ts +26 -0
- package/src/host/pendingHostComponentStore.ts +241 -0
- package/src/host/resolveParams.ts +70 -0
- package/src/host/routeMatcher.ts +117 -0
- package/src/host/types.ts +45 -0
- package/src/index.ts +57 -0
- package/src/plugin/index.ts +2 -0
- package/src/plugin/intoShared.ts +50 -0
- package/src/plugin/microFrontendPlugin.ts +57 -0
- package/src/plugin/prelude.ts +73 -0
- package/src/plugin/resolver.ts +73 -0
- package/src/plugin/types.ts +6 -0
- package/src/runtime/createMicroFrontendRuntime.ts +115 -0
- package/src/runtime/errors.ts +86 -0
- package/src/runtime/parseAppRequest.ts +22 -0
- package/src/runtime/parseNativeRuntimeEvent.ts +54 -0
- package/src/runtime/registry.ts +131 -0
- package/src/session/MicroFrontendSessionContext.tsx +45 -0
- package/src/session/useMicroFrontendSessions.ts +73 -0
- package/src/specs/NativeGraniteMicroFrontendRuntime.ts +44 -0
- package/src/specs/PortalHostViewNativeComponent.ts +9 -0
- package/src/specs/PortalViewNativeComponent.ts +9 -0
- package/src/types.ts +55 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <fbjni/fbjni.h>
|
|
4
|
+
|
|
5
|
+
namespace granite::microfrontend {
|
|
6
|
+
|
|
7
|
+
struct BundleEvaluator : facebook::jni::HybridClass<BundleEvaluator> {
|
|
8
|
+
static constexpr auto kJavaDescriptor =
|
|
9
|
+
"Lrun/granite/microfrontend/BundleEvaluator;";
|
|
10
|
+
|
|
11
|
+
static void registerNatives();
|
|
12
|
+
|
|
13
|
+
static void evaluateFileSync(
|
|
14
|
+
facebook::jni::alias_ref<jhybridobject>,
|
|
15
|
+
jlong runtimePointer,
|
|
16
|
+
facebook::jni::alias_ref<facebook::jni::JString> filePath,
|
|
17
|
+
facebook::jni::alias_ref<facebook::jni::JString> sourceUrl);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
} // namespace granite::microfrontend
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#include "FileReader.h"
|
|
2
|
+
|
|
3
|
+
#include <cerrno>
|
|
4
|
+
#include <cstring>
|
|
5
|
+
#include <fcntl.h>
|
|
6
|
+
#include <new>
|
|
7
|
+
#include <sys/stat.h>
|
|
8
|
+
#include <unistd.h>
|
|
9
|
+
|
|
10
|
+
namespace granite::microfrontend::io {
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
class FileDescriptor final {
|
|
14
|
+
public:
|
|
15
|
+
explicit FileDescriptor(int value) : value_(value) {}
|
|
16
|
+
~FileDescriptor() { close(value_); }
|
|
17
|
+
|
|
18
|
+
FileDescriptor(const FileDescriptor &) = delete;
|
|
19
|
+
FileDescriptor &operator=(const FileDescriptor &) = delete;
|
|
20
|
+
|
|
21
|
+
int get() const { return value_; }
|
|
22
|
+
|
|
23
|
+
private:
|
|
24
|
+
int value_;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
std::string errorMessage(const std::string &message, const std::string &path,
|
|
28
|
+
int errorNumber) {
|
|
29
|
+
return message + ": " + path + " (" + std::strerror(errorNumber) + ")";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
FileReaderError::FileReaderError(FileReaderErrorKind kind,
|
|
35
|
+
const std::string &message)
|
|
36
|
+
: std::runtime_error(message), kind_(kind) {}
|
|
37
|
+
|
|
38
|
+
FileReaderErrorKind FileReaderError::kind() const { return kind_; }
|
|
39
|
+
|
|
40
|
+
std::string readFileToMemory(const std::string &path) {
|
|
41
|
+
std::string source;
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
const int fileDescriptor = open(path.c_str(), O_RDONLY);
|
|
45
|
+
if (fileDescriptor < 0) {
|
|
46
|
+
const int errorNumber = errno;
|
|
47
|
+
throw FileReaderError(
|
|
48
|
+
FileReaderErrorKind::NotFound,
|
|
49
|
+
errorMessage("Bundle file cannot be opened", path, errorNumber));
|
|
50
|
+
}
|
|
51
|
+
FileDescriptor file(fileDescriptor);
|
|
52
|
+
|
|
53
|
+
struct stat fileStat {};
|
|
54
|
+
if (fstat(file.get(), &fileStat) < 0) {
|
|
55
|
+
const int errorNumber = errno;
|
|
56
|
+
throw FileReaderError(
|
|
57
|
+
FileReaderErrorKind::StatFailed,
|
|
58
|
+
errorMessage("Bundle file cannot be inspected", path, errorNumber));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
source.resize(static_cast<size_t>(fileStat.st_size));
|
|
63
|
+
} catch (const std::bad_alloc &) {
|
|
64
|
+
throw FileReaderError(FileReaderErrorKind::AllocationFailed,
|
|
65
|
+
"Bundle file cannot fit in memory: " + path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
size_t offset = 0;
|
|
69
|
+
while (offset < source.size()) {
|
|
70
|
+
const ssize_t bytesRead =
|
|
71
|
+
read(file.get(), source.data() + offset, source.size() - offset);
|
|
72
|
+
if (bytesRead < 0) {
|
|
73
|
+
if (errno == EINTR) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const int errorNumber = errno;
|
|
77
|
+
throw FileReaderError(
|
|
78
|
+
FileReaderErrorKind::ReadFailed,
|
|
79
|
+
errorMessage("Bundle file cannot be read", path, errorNumber));
|
|
80
|
+
}
|
|
81
|
+
if (bytesRead == 0) {
|
|
82
|
+
source.resize(offset);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
offset += static_cast<size_t>(bytesRead);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return source;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <stdexcept>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
namespace granite::microfrontend::io {
|
|
7
|
+
|
|
8
|
+
enum class FileReaderErrorKind {
|
|
9
|
+
NotFound,
|
|
10
|
+
StatFailed,
|
|
11
|
+
AllocationFailed,
|
|
12
|
+
ReadFailed,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class FileReaderError : public std::runtime_error {
|
|
16
|
+
public:
|
|
17
|
+
FileReaderError(FileReaderErrorKind kind, const std::string &message);
|
|
18
|
+
|
|
19
|
+
FileReaderErrorKind kind() const;
|
|
20
|
+
|
|
21
|
+
private:
|
|
22
|
+
FileReaderErrorKind kind_;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
std::string readFileToMemory(const std::string &path);
|
|
26
|
+
|
|
27
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
package com.teleport.extensions
|
|
2
|
+
|
|
3
|
+
import android.content.res.Resources
|
|
4
|
+
|
|
5
|
+
internal val Float.dp: Double
|
|
6
|
+
get() = (this / displayDensity).toDouble()
|
|
7
|
+
|
|
8
|
+
internal val Float.px: Double
|
|
9
|
+
get() = (this * displayDensity).toDouble()
|
|
10
|
+
|
|
11
|
+
private val displayDensity: Float
|
|
12
|
+
get() = Resources.getSystem().displayMetrics.density
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
package com.teleport.global
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
5
|
+
import com.teleport.host.PortalHostView
|
|
6
|
+
import com.teleport.portal.PortalView
|
|
7
|
+
import java.lang.ref.WeakReference
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Matches source [PortalView] instances with destination [PortalHostView] instances by name.
|
|
11
|
+
*
|
|
12
|
+
* The registry is confined to the Android UI thread because it stores and invokes [View]
|
|
13
|
+
* instances. Callers must not access it from background threads.
|
|
14
|
+
*/
|
|
15
|
+
object PortalRegistry {
|
|
16
|
+
private val hosts: MutableMap<String, MutableList<WeakReference<PortalHostView>>> = HashMap()
|
|
17
|
+
private val pendingPortals: MutableMap<String, MutableList<WeakReference<PortalView>>> = HashMap()
|
|
18
|
+
|
|
19
|
+
fun registerHost(
|
|
20
|
+
name: String,
|
|
21
|
+
view: PortalHostView,
|
|
22
|
+
) {
|
|
23
|
+
UiThreadUtil.assertOnUiThread()
|
|
24
|
+
val namedHosts = hosts.getOrPut(name) { mutableListOf() }
|
|
25
|
+
namedHosts.removeAll { it.get() == null || it.get() === view }
|
|
26
|
+
namedHosts.add(WeakReference(view))
|
|
27
|
+
notifySubscribers(name)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fun unregisterHost(
|
|
31
|
+
name: String,
|
|
32
|
+
view: PortalHostView,
|
|
33
|
+
) {
|
|
34
|
+
UiThreadUtil.assertOnUiThread()
|
|
35
|
+
hosts[name]?.let { namedHosts ->
|
|
36
|
+
namedHosts.removeAll { it.get() == null || it.get() === view }
|
|
37
|
+
if (namedHosts.isEmpty()) {
|
|
38
|
+
hosts.remove(name)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
notifySubscribers(name)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private fun notifySubscribers(name: String) {
|
|
45
|
+
pendingPortals[name]?.let { portals ->
|
|
46
|
+
val iterator = portals.iterator()
|
|
47
|
+
while (iterator.hasNext()) {
|
|
48
|
+
val portalRef = iterator.next()
|
|
49
|
+
portalRef.get()?.onHostChanged() ?: iterator.remove()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun notifyHostLayoutChanged(name: String) {
|
|
55
|
+
UiThreadUtil.assertOnUiThread()
|
|
56
|
+
pendingPortals[name]?.let { portals ->
|
|
57
|
+
val iterator = portals.iterator()
|
|
58
|
+
while (iterator.hasNext()) {
|
|
59
|
+
val portalRef = iterator.next()
|
|
60
|
+
portalRef.get()?.onHostLayoutChanged() ?: iterator.remove()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Prefer an attached destination in the source View's Window. If the source
|
|
67
|
+
* belongs to a detached controller surface, or no same-window destination
|
|
68
|
+
* exists, use the most recently registered attached host from another Window.
|
|
69
|
+
*/
|
|
70
|
+
fun resolveHost(
|
|
71
|
+
name: String?,
|
|
72
|
+
sourceView: View,
|
|
73
|
+
): PortalHostView? {
|
|
74
|
+
UiThreadUtil.assertOnUiThread()
|
|
75
|
+
if (name == null) return null
|
|
76
|
+
|
|
77
|
+
val namedHosts = hosts[name] ?: return null
|
|
78
|
+
namedHosts.removeAll { it.get() == null }
|
|
79
|
+
if (namedHosts.isEmpty()) {
|
|
80
|
+
hosts.remove(name)
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
val liveHosts = namedHosts.mapNotNull { it.get() }
|
|
85
|
+
val sourceWindowToken = sourceView.windowToken
|
|
86
|
+
if (sourceView.isAttachedToWindow && sourceWindowToken != null) {
|
|
87
|
+
liveHosts.lastOrNull {
|
|
88
|
+
it.isAttachedToWindow && it.windowToken == sourceWindowToken
|
|
89
|
+
}?.let { return it }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return liveHosts.lastOrNull { it.isAttachedToWindow }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fun registerPendingPortal(
|
|
96
|
+
hostName: String,
|
|
97
|
+
portal: PortalView,
|
|
98
|
+
) {
|
|
99
|
+
UiThreadUtil.assertOnUiThread()
|
|
100
|
+
val portals = pendingPortals.getOrPut(hostName) { mutableListOf() }
|
|
101
|
+
portals.removeAll { it.get() == null || it.get() == portal }
|
|
102
|
+
portals.add(WeakReference(portal))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
fun unregisterPendingPortal(
|
|
106
|
+
hostName: String,
|
|
107
|
+
portal: PortalView,
|
|
108
|
+
) {
|
|
109
|
+
UiThreadUtil.assertOnUiThread()
|
|
110
|
+
pendingPortals[hostName]?.let { portals ->
|
|
111
|
+
portals.removeAll { it.get() == null || it.get() == portal }
|
|
112
|
+
if (portals.isEmpty()) {
|
|
113
|
+
pendingPortals.remove(hostName)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
package com.teleport.host
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
7
|
+
import com.teleport.global.PortalRegistry
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Native-owned destination for children moved out of a [com.teleport.portal.PortalView].
|
|
11
|
+
*
|
|
12
|
+
* A named host registers while attached to a Window. The matching source view keeps React/Fabric
|
|
13
|
+
* ownership of its children and only reparents their Android views into this container.
|
|
14
|
+
*/
|
|
15
|
+
class PortalHostView(
|
|
16
|
+
context: Context?,
|
|
17
|
+
) : ReactViewGroup(context) {
|
|
18
|
+
private var name: String? = null
|
|
19
|
+
private var isInBatch = false
|
|
20
|
+
private var batchBaseIndex = 0
|
|
21
|
+
private var hasPendingCleanup = false
|
|
22
|
+
|
|
23
|
+
fun setName(newName: String?) {
|
|
24
|
+
if (name == newName) return
|
|
25
|
+
|
|
26
|
+
hasPendingCleanup = false
|
|
27
|
+
name?.let { PortalRegistry.unregisterHost(it, this) }
|
|
28
|
+
name = newName
|
|
29
|
+
newName?.let { PortalRegistry.registerHost(it, this) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fun cleanup() {
|
|
33
|
+
if (isAttachedToWindow) {
|
|
34
|
+
hasPendingCleanup = true
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cleanupNow()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns the index at which a portal child should be inserted.
|
|
43
|
+
*
|
|
44
|
+
* Within a single Fabric commit all mutations run synchronously on the main
|
|
45
|
+
* thread. The first call in a commit records the current child count as
|
|
46
|
+
* the "base"; subsequent calls in the same commit reuse that base so that
|
|
47
|
+
* bottom-to-top Fabric ordering is compensated by [addView] at a specific index.
|
|
48
|
+
* A [Handler.post] resets the flag after the commit finishes.
|
|
49
|
+
*/
|
|
50
|
+
fun nextInsertionIndexForChildAt(childIndex: Int): Int {
|
|
51
|
+
if (!isInBatch) {
|
|
52
|
+
isInBatch = true
|
|
53
|
+
batchBaseIndex = childCount
|
|
54
|
+
Handler(Looper.getMainLooper()).post { isInBatch = false }
|
|
55
|
+
}
|
|
56
|
+
return minOf(batchBaseIndex + childIndex, childCount)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun onAttachedToWindow() {
|
|
60
|
+
super.onAttachedToWindow()
|
|
61
|
+
// A host can be named before its Activity installs it into the Window.
|
|
62
|
+
// Register again on attach so detached controller surfaces resolve the
|
|
63
|
+
// currently visible Activity host deterministically.
|
|
64
|
+
name?.let { PortalRegistry.registerHost(it, this) }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override fun onLayout(
|
|
68
|
+
changed: Boolean,
|
|
69
|
+
left: Int,
|
|
70
|
+
top: Int,
|
|
71
|
+
right: Int,
|
|
72
|
+
bottom: Int,
|
|
73
|
+
) {
|
|
74
|
+
super.onLayout(changed, left, top, right, bottom)
|
|
75
|
+
name?.let { PortalRegistry.notifyHostLayoutChanged(it) }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override fun onDetachedFromWindow() {
|
|
79
|
+
super.onDetachedFromWindow()
|
|
80
|
+
|
|
81
|
+
if (hasPendingCleanup) {
|
|
82
|
+
cleanupNow()
|
|
83
|
+
} else {
|
|
84
|
+
// Keep the name so a temporary detach can register again on attach, but
|
|
85
|
+
// stop routing children into a host that is no longer visible.
|
|
86
|
+
name?.let { PortalRegistry.unregisterHost(it, this) }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private fun cleanupNow() {
|
|
91
|
+
name?.let { PortalRegistry.unregisterHost(it, this) }
|
|
92
|
+
name = null
|
|
93
|
+
isInBatch = false
|
|
94
|
+
batchBaseIndex = 0
|
|
95
|
+
hasPendingCleanup = false
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
package com.teleport.host
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.module.annotations.ReactModule
|
|
4
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
5
|
+
import com.facebook.react.uimanager.ViewManagerDelegate
|
|
6
|
+
import com.facebook.react.uimanager.annotations.ReactProp
|
|
7
|
+
import com.facebook.react.viewmanagers.PortalHostViewManagerDelegate
|
|
8
|
+
import com.facebook.react.viewmanagers.PortalHostViewManagerInterface
|
|
9
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
10
|
+
import com.teleport.managers.TeleportViewManager
|
|
11
|
+
|
|
12
|
+
@ReactModule(name = PortalHostViewManager.NAME)
|
|
13
|
+
class PortalHostViewManager :
|
|
14
|
+
TeleportViewManager(),
|
|
15
|
+
PortalHostViewManagerInterface<ReactViewGroup> {
|
|
16
|
+
private val delegate = PortalHostViewManagerDelegate(this)
|
|
17
|
+
|
|
18
|
+
override fun getName(): String = NAME
|
|
19
|
+
|
|
20
|
+
override fun getDelegate(): ViewManagerDelegate<ReactViewGroup> = delegate
|
|
21
|
+
|
|
22
|
+
override fun createTeleportView(context: ThemedReactContext): ReactViewGroup = PortalHostView(context)
|
|
23
|
+
|
|
24
|
+
override fun onDropViewInstance(view: ReactViewGroup) {
|
|
25
|
+
super.onDropViewInstance(view)
|
|
26
|
+
|
|
27
|
+
(view as? PortalHostView)?.cleanup()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@ReactProp(name = "name")
|
|
31
|
+
override fun setName(
|
|
32
|
+
view: ReactViewGroup?,
|
|
33
|
+
name: String?,
|
|
34
|
+
) {
|
|
35
|
+
(view as? PortalHostView)?.setName(name)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
companion object {
|
|
39
|
+
const val NAME = "PortalHostView"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
package com.teleport.host
|
|
2
|
+
|
|
3
|
+
import android.view.MotionEvent
|
|
4
|
+
import android.view.View
|
|
5
|
+
import com.facebook.react.ReactHost
|
|
6
|
+
import com.facebook.react.ReactRootView
|
|
7
|
+
import com.facebook.react.bridge.ReactContext
|
|
8
|
+
import com.facebook.react.config.ReactFeatureFlags
|
|
9
|
+
import com.facebook.react.uimanager.JSPointerDispatcher
|
|
10
|
+
import com.facebook.react.uimanager.JSTouchDispatcher
|
|
11
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
12
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
13
|
+
import com.facebook.react.uimanager.common.UIManagerType
|
|
14
|
+
import com.facebook.react.uimanager.events.EventDispatcher
|
|
15
|
+
|
|
16
|
+
class PortalReactRootView(
|
|
17
|
+
context: ThemedReactContext,
|
|
18
|
+
private val reactHost: ReactHost,
|
|
19
|
+
surfaceId: Int,
|
|
20
|
+
private val moduleName: String,
|
|
21
|
+
) : ReactRootView(context) {
|
|
22
|
+
private val touchDispatcher = JSTouchDispatcher(this)
|
|
23
|
+
private val pointerDispatcher =
|
|
24
|
+
if (ReactFeatureFlags.dispatchPointerEvents) {
|
|
25
|
+
JSPointerDispatcher(this)
|
|
26
|
+
} else {
|
|
27
|
+
null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private val eventDispatcher: EventDispatcher?
|
|
31
|
+
get() =
|
|
32
|
+
reactHost.currentReactContext?.let {
|
|
33
|
+
UIManagerHelper.getEventDispatcher(it, UIManagerType.FABRIC)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
init {
|
|
37
|
+
setIsFabric(true)
|
|
38
|
+
setRootViewTag(surfaceId)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
override fun onMeasure(
|
|
42
|
+
widthMeasureSpec: Int,
|
|
43
|
+
heightMeasureSpec: Int,
|
|
44
|
+
) {
|
|
45
|
+
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
|
46
|
+
|
|
47
|
+
val childWidthSpec = MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY)
|
|
48
|
+
val childHeightSpec = MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)
|
|
49
|
+
for (index in 0 until childCount) {
|
|
50
|
+
getChildAt(index).measure(childWidthSpec, childHeightSpec)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun onLayout(
|
|
55
|
+
changed: Boolean,
|
|
56
|
+
left: Int,
|
|
57
|
+
top: Int,
|
|
58
|
+
right: Int,
|
|
59
|
+
bottom: Int,
|
|
60
|
+
) {
|
|
61
|
+
for (index in 0 until childCount) {
|
|
62
|
+
getChildAt(index).layout(0, 0, right - left, bottom - top)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
override fun onChildStartedNativeGesture(
|
|
67
|
+
childView: View?,
|
|
68
|
+
ev: MotionEvent,
|
|
69
|
+
) {
|
|
70
|
+
val dispatcher = eventDispatcher ?: return
|
|
71
|
+
touchDispatcher.onChildStartedNativeGesture(ev, dispatcher)
|
|
72
|
+
childView?.let {
|
|
73
|
+
pointerDispatcher?.onChildStartedNativeGesture(it, ev, dispatcher)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onChildEndedNativeGesture(
|
|
78
|
+
childView: View,
|
|
79
|
+
ev: MotionEvent,
|
|
80
|
+
) {
|
|
81
|
+
val dispatcher = eventDispatcher ?: return
|
|
82
|
+
touchDispatcher.onChildEndedNativeGesture(ev, dispatcher)
|
|
83
|
+
pointerDispatcher?.onChildEndedNativeGesture()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
override fun handleException(t: Throwable) {
|
|
87
|
+
val exception = if (t is Exception) t else RuntimeException(t)
|
|
88
|
+
currentReactContext?.handleException(exception) ?: throw exception
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
override fun getUIManagerType(): Int = UIManagerType.FABRIC
|
|
92
|
+
|
|
93
|
+
override fun getJSModuleName(): String = moduleName
|
|
94
|
+
|
|
95
|
+
override fun dispatchJSTouchEvent(event: MotionEvent) {
|
|
96
|
+
val dispatcher = eventDispatcher ?: return
|
|
97
|
+
touchDispatcher.handleTouchEvent(event, dispatcher, currentReactContext)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
override fun dispatchJSPointerEvent(
|
|
101
|
+
event: MotionEvent,
|
|
102
|
+
isCapture: Boolean,
|
|
103
|
+
) {
|
|
104
|
+
val dispatcher = eventDispatcher ?: return
|
|
105
|
+
pointerDispatcher?.handleMotionEvent(event, dispatcher, isCapture)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
override fun hasActiveReactContext(): Boolean =
|
|
109
|
+
currentReactContext?.hasActiveReactInstance() == true
|
|
110
|
+
|
|
111
|
+
override fun hasActiveReactInstance(): Boolean = hasActiveReactContext()
|
|
112
|
+
|
|
113
|
+
override fun getCurrentReactContext(): ReactContext? = reactHost.currentReactContext
|
|
114
|
+
|
|
115
|
+
override fun isViewAttachedToReactInstance(): Boolean = hasActiveReactContext()
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
package com.teleport.managers
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.uimanager.ThemedReactContext
|
|
4
|
+
import com.facebook.react.views.view.ReactViewGroup
|
|
5
|
+
import com.facebook.react.views.view.ReactViewManager
|
|
6
|
+
|
|
7
|
+
abstract class TeleportViewManager : ReactViewManager() {
|
|
8
|
+
protected abstract fun createTeleportView(context: ThemedReactContext): ReactViewGroup
|
|
9
|
+
|
|
10
|
+
fun forceBoxNone(view: ReactViewGroup) {
|
|
11
|
+
super.setPointerEvents(view, "box-none")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun createViewInstance(context: ThemedReactContext): ReactViewGroup =
|
|
15
|
+
createTeleportView(context).also {
|
|
16
|
+
forceBoxNone(it)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun prepareToRecycleView(
|
|
20
|
+
reactContext: ThemedReactContext,
|
|
21
|
+
view: ReactViewGroup,
|
|
22
|
+
): ReactViewGroup {
|
|
23
|
+
super.prepareToRecycleView(reactContext, view)
|
|
24
|
+
forceBoxNone(view)
|
|
25
|
+
return view
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
package com.teleport.portal
|
|
2
|
+
|
|
3
|
+
import android.view.View
|
|
4
|
+
import com.facebook.react.bridge.Arguments
|
|
5
|
+
import com.facebook.react.uimanager.StateWrapper
|
|
6
|
+
import com.teleport.extensions.dp
|
|
7
|
+
import com.teleport.extensions.isDetached
|
|
8
|
+
import com.teleport.extensions.screenLocation
|
|
9
|
+
import com.teleport.host.PortalHostView
|
|
10
|
+
|
|
11
|
+
internal class PortalLayoutStateController(
|
|
12
|
+
private val sourceView: View,
|
|
13
|
+
) {
|
|
14
|
+
private var lastLayout = PortalLayoutState.EMPTY
|
|
15
|
+
private var stateWrapper: StateWrapper? = null
|
|
16
|
+
|
|
17
|
+
fun setStateWrapper(wrapper: StateWrapper?) {
|
|
18
|
+
stateWrapper = wrapper
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
fun updateIfNeeded(
|
|
22
|
+
hostName: String?,
|
|
23
|
+
host: PortalHostView?,
|
|
24
|
+
) {
|
|
25
|
+
val wrapper = stateWrapper
|
|
26
|
+
val activeHost = activeHost(hostName, host)
|
|
27
|
+
|
|
28
|
+
when {
|
|
29
|
+
wrapper == null -> Unit
|
|
30
|
+
activeHost == null -> resetIfNeeded()
|
|
31
|
+
else -> publishIfChanged(wrapper, createLayoutState(activeHost))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun resetIfNeeded() {
|
|
36
|
+
val wrapper = stateWrapper
|
|
37
|
+
|
|
38
|
+
if (wrapper != null) {
|
|
39
|
+
publishIfChanged(wrapper, PortalLayoutState.EMPTY)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun activeHost(
|
|
44
|
+
hostName: String?,
|
|
45
|
+
host: PortalHostView?,
|
|
46
|
+
): PortalHostView? =
|
|
47
|
+
if (hostName == null || host == null || host.isDetached()) {
|
|
48
|
+
null
|
|
49
|
+
} else {
|
|
50
|
+
host
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private fun publishIfChanged(
|
|
54
|
+
wrapper: StateWrapper,
|
|
55
|
+
nextLayout: PortalLayoutState,
|
|
56
|
+
) {
|
|
57
|
+
if (lastLayout != nextLayout) {
|
|
58
|
+
lastLayout = nextLayout
|
|
59
|
+
wrapper.updateState(nextLayout.toMap())
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private fun createLayoutState(host: PortalHostView): PortalLayoutState {
|
|
64
|
+
val hostLocation = host.screenLocation()
|
|
65
|
+
// A detached controller surface has no Window coordinate space. Its children
|
|
66
|
+
// are already physically parented by the destination host, so use the host
|
|
67
|
+
// origin as the logical source origin and avoid applying a screen inset twice.
|
|
68
|
+
val sourceLocation =
|
|
69
|
+
if (sourceView.isDetached()) {
|
|
70
|
+
hostLocation
|
|
71
|
+
} else {
|
|
72
|
+
sourceView.screenLocation()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return PortalLayoutState(
|
|
76
|
+
hostWidth = host.width.toFloat().dp,
|
|
77
|
+
hostHeight = host.height.toFloat().dp,
|
|
78
|
+
offsetX = (hostLocation[0] - sourceLocation[0]).toFloat().dp,
|
|
79
|
+
offsetY = (hostLocation[1] - sourceLocation[1]).toFloat().dp,
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private data class PortalLayoutState(
|
|
85
|
+
val hostWidth: Double,
|
|
86
|
+
val hostHeight: Double,
|
|
87
|
+
val offsetX: Double,
|
|
88
|
+
val offsetY: Double,
|
|
89
|
+
) {
|
|
90
|
+
fun toMap() =
|
|
91
|
+
Arguments.createMap().apply {
|
|
92
|
+
putDouble("hostWidth", hostWidth)
|
|
93
|
+
putDouble("hostHeight", hostHeight)
|
|
94
|
+
putDouble("offsetX", offsetX)
|
|
95
|
+
putDouble("offsetY", offsetY)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
companion object {
|
|
99
|
+
val EMPTY =
|
|
100
|
+
PortalLayoutState(
|
|
101
|
+
hostWidth = 0.0,
|
|
102
|
+
hostHeight = 0.0,
|
|
103
|
+
offsetX = 0.0,
|
|
104
|
+
offsetY = 0.0,
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|