feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,252 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type IntersectionObserverEntry from './IntersectionObserverEntry';
import type {IntersectionObserverId} from './IntersectionObserverManager';
import ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import * as IntersectionObserverManager from './IntersectionObserverManager';
export type IntersectionObserverCallback = (
entries: Array<IntersectionObserverEntry>,
observer: IntersectionObserver,
) => mixed;
type IntersectionObserverInit = {
// root?: ReactNativeElement, // This option exists on the Web but it's not currently supported in React Native.
// rootMargin?: string, // This option exists on the Web but it's not currently supported in React Native.
threshold?: number | $ReadOnlyArray<number>,
};
/**
* The [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API)
* provides a way to asynchronously observe changes in the intersection of a
* target element with an ancestor element or with a top-level document's
* viewport.
*
* The ancestor element or viewport is referred to as the root.
*
* When an `IntersectionObserver` is created, it's configured to watch for given
* ratios of visibility within the root.
*
* The configuration cannot be changed once the `IntersectionObserver` is
* created, so a given observer object is only useful for watching for specific
* changes in degree of visibility; however, you can watch multiple target
* elements with the same observer.
*
* This implementation only supports the `threshold` option at the moment
* (`root` and `rootMargin` are not supported).
*/
export default class IntersectionObserver {
_callback: IntersectionObserverCallback;
_thresholds: $ReadOnlyArray<number>;
_observationTargets: Set<ReactNativeElement> = new Set();
_intersectionObserverId: ?IntersectionObserverId;
constructor(
callback: IntersectionObserverCallback,
options?: IntersectionObserverInit,
): void {
if (callback == null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': 1 argument required, but only 0 present.",
);
}
if (typeof callback !== 'function') {
throw new TypeError(
"Failed to construct 'IntersectionObserver': parameter 1 is not of type 'Function'.",
);
}
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.root != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': root is not supported",
);
}
// $FlowExpectedError[prop-missing] it's not typed in React Native but exists on Web.
if (options?.rootMargin != null) {
throw new TypeError(
"Failed to construct 'IntersectionObserver': rootMargin is not supported",
);
}
this._callback = callback;
this._thresholds = normalizeThresholds(options?.threshold);
}
/**
* The `ReactNativeElement` whose bounds are used as the bounding box when
* testing for intersection.
* If no `root` value was passed to the constructor or its value is `null`,
* the root view is used.
*
* NOTE: This cannot currently be configured and `root` is always `null`.
*/
get root(): ReactNativeElement | null {
return null;
}
/**
* String with syntax similar to that of the CSS `margin` property.
* Each side of the rectangle represented by `rootMargin` is added to the
* corresponding side in the root element's bounding box before the
* intersection test is performed.
*
* NOTE: This cannot currently be configured and `rootMargin` is always
* `null`.
*/
get rootMargin(): string {
return '0px 0px 0px 0px';
}
/**
* A list of thresholds, sorted in increasing numeric order, where each
* threshold is a ratio of intersection area to bounding box area of an
* observed target.
* Notifications for a target are generated when any of the thresholds are
* crossed for that target.
* If no value was passed to the constructor, `0` is used.
*/
get thresholds(): $ReadOnlyArray<number> {
return this._thresholds;
}
/**
* Adds an element to the set of target elements being watched by the
* `IntersectionObserver`.
* One observer has one set of thresholds and one root, but can watch multiple
* target elements for visibility changes.
* To stop observing the element, call `IntersectionObserver.unobserve()`.
*/
observe(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (this._observationTargets.has(target)) {
return;
}
IntersectionObserverManager.observe({
intersectionObserverId: this._getOrCreateIntersectionObserverId(),
target,
});
this._observationTargets.add(target);
}
/**
* Instructs the `IntersectionObserver` to stop observing the specified target
* element.
*/
unobserve(target: ReactNativeElement): void {
if (!(target instanceof ReactNativeElement)) {
throw new TypeError(
"Failed to execute 'unobserve' on 'IntersectionObserver': parameter 1 is not of type 'ReactNativeElement'.",
);
}
if (!this._observationTargets.has(target)) {
return;
}
const intersectionObserverId = this._intersectionObserverId;
if (intersectionObserverId == null) {
// This is unexpected if the target is in `_observationTargets`.
console.error(
"Unexpected state in 'IntersectionObserver': could not find observer ID to unobserve target.",
);
return;
}
IntersectionObserverManager.unobserve(intersectionObserverId, target);
this._observationTargets.delete(target);
if (this._observationTargets.size === 0) {
IntersectionObserverManager.unregisterObserver(intersectionObserverId);
this._intersectionObserverId = null;
}
}
/**
* Stops watching all of its target elements for visibility changes.
*/
disconnect(): void {
for (const target of this._observationTargets.keys()) {
this.unobserve(target);
}
}
_getOrCreateIntersectionObserverId(): IntersectionObserverId {
let intersectionObserverId = this._intersectionObserverId;
if (intersectionObserverId == null) {
intersectionObserverId = IntersectionObserverManager.registerObserver(
this,
this._callback,
);
this._intersectionObserverId = intersectionObserverId;
}
return intersectionObserverId;
}
// Only for tests
__getObserverID(): ?IntersectionObserverId {
return this._intersectionObserverId;
}
}
/**
* Converts the user defined `threshold` value into an array of sorted valid
* threshold options for `IntersectionObserver` (double ∈ [0, 1]).
*
* @example
* normalizeThresholds(0.5); // → [0.5]
* normalizeThresholds([1, 0.5, 0]); // → [0, 0.5, 1]
* normalizeThresholds(['1', '0.5', '0']); // → [0, 0.5, 1]
*/
function normalizeThresholds(threshold: mixed): $ReadOnlyArray<number> {
if (Array.isArray(threshold)) {
if (threshold.length > 0) {
return threshold.map(normalizeThresholdValue).sort();
} else {
return [0];
}
}
return [normalizeThresholdValue(threshold)];
}
function normalizeThresholdValue(threshold: mixed): number {
if (threshold == null) {
return 0;
}
const thresholdAsNumber = Number(threshold);
if (!Number.isFinite(thresholdAsNumber)) {
throw new TypeError(
"Failed to read the 'threshold' property from 'IntersectionObserverInit': The provided double value is non-finite.",
);
}
if (thresholdAsNumber < 0 || thresholdAsNumber > 1) {
throw new RangeError(
"Failed to construct 'IntersectionObserver': Threshold values must be numbers between 0 and 1",
);
}
return thresholdAsNumber;
}

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
// flowlint unsafe-getters-setters:off
import type ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import type {NativeIntersectionObserverEntry} from './NativeIntersectionObserver';
import DOMRectReadOnly from '../../src/private/webapis/dom/geometry/DOMRectReadOnly';
/**
* The [`IntersectionObserverEntry`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry)
* interface of the Intersection Observer API describes the intersection between
* the target element and its root container at a specific moment of transition.
*
* An array of `IntersectionObserverEntry` is delivered to
* `IntersectionObserver` callbacks as the first argument.
*/
export default class IntersectionObserverEntry {
// We lazily compute all the properties from the raw entry provided by the
// native module, so we avoid unnecessary work.
_nativeEntry: NativeIntersectionObserverEntry;
// There are cases where this cannot be safely derived from the instance
// handle in the native entry (when the target is detached), so we need to
// keep a reference to it directly.
_target: ReactNativeElement;
constructor(
nativeEntry: NativeIntersectionObserverEntry,
target: ReactNativeElement,
) {
this._nativeEntry = nativeEntry;
this._target = target;
}
/**
* Returns the bounds rectangle of the target element as a `DOMRectReadOnly`.
* The bounds are computed as described in the documentation for
* [`Element.getBoundingClientRect()`](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
*/
get boundingClientRect(): DOMRectReadOnly {
const targetRect = this._nativeEntry.targetRect;
return new DOMRectReadOnly(
targetRect[0],
targetRect[1],
targetRect[2],
targetRect[3],
);
}
/**
* Returns the ratio of the `intersectionRect` to the `boundingClientRect`.
*/
get intersectionRatio(): number {
const intersectionRect = this.intersectionRect;
const boundingClientRect = this.boundingClientRect;
if (boundingClientRect.width === 0 || boundingClientRect.height === 0) {
return 0;
}
const ratio =
(intersectionRect.width * intersectionRect.height) /
(boundingClientRect.width * boundingClientRect.height);
// Prevent rounding errors from making this value greater than 1.
return Math.min(ratio, 1);
}
/**
* Returns a `DOMRectReadOnly` representing the target's visible area.
*/
get intersectionRect(): DOMRectReadOnly {
const intersectionRect = this._nativeEntry.intersectionRect;
if (intersectionRect == null) {
return new DOMRectReadOnly();
}
return new DOMRectReadOnly(
intersectionRect[0],
intersectionRect[1],
intersectionRect[2],
intersectionRect[3],
);
}
/**
* A `Boolean` value which is `true` if the target element intersects with the
* intersection observer's root.
* * If this is `true`, then, the `IntersectionObserverEntry` describes a
* transition into a state of intersection.
* * If it's `false`, then you know the transition is from intersecting to
* not-intersecting.
*/
get isIntersecting(): boolean {
return this._nativeEntry.isIntersectingAboveThresholds;
}
/**
* Returns a `DOMRectReadOnly` for the intersection observer's root.
*/
get rootBounds(): DOMRectReadOnly {
const rootRect = this._nativeEntry.rootRect;
return new DOMRectReadOnly(
rootRect[0],
rootRect[1],
rootRect[2],
rootRect[3],
);
}
/**
* The `ReactNativeElement` whose intersection with the root changed.
*/
get target(): ReactNativeElement {
return this._target;
}
/**
* A `DOMHighResTimeStamp` indicating the time at which the intersection was
* recorded, relative to the `IntersectionObserver`'s time origin.
*/
get time(): DOMHighResTimeStamp {
return this._nativeEntry.time;
}
}
export function createIntersectionObserverEntry(
entry: NativeIntersectionObserverEntry,
target: ReactNativeElement,
): IntersectionObserverEntry {
return new IntersectionObserverEntry(entry, target);
}

View File

@@ -0,0 +1,299 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
/**
* This module handles the communication between the React Native renderer
* and all the intersection observers that are currently observing any targets.
*
* In order to reduce the communication between native and JavaScript,
* we register a single notication callback in native, and then we handle how
* to notify each entry to the right intersection observer when we receive all
* the notifications together.
*/
import type ReactNativeElement from '../../src/private/webapis/dom/nodes/ReactNativeElement';
import type IntersectionObserver, {
IntersectionObserverCallback,
} from './IntersectionObserver';
import type IntersectionObserverEntry from './IntersectionObserverEntry';
import {
getInstanceHandle,
getShadowNode,
} from '../../src/private/webapis/dom/nodes/ReadOnlyNode';
import * as Systrace from '../Performance/Systrace';
import warnOnce from '../Utilities/warnOnce';
import {createIntersectionObserverEntry} from './IntersectionObserverEntry';
import NativeIntersectionObserver from './NativeIntersectionObserver';
export type IntersectionObserverId = number;
let nextIntersectionObserverId: IntersectionObserverId = 1;
let isConnected: boolean = false;
const registeredIntersectionObservers: Map<
IntersectionObserverId,
{observer: IntersectionObserver, callback: IntersectionObserverCallback},
> = new Map();
// We need to keep the mapping from instance handles to targets because when
// targets are detached (their components are unmounted), React resets the
// instance handle to prevent memory leaks and it cuts the connection between
// the instance handle and the target.
const instanceHandleToTargetMap: WeakMap<interface {}, ReactNativeElement> =
new WeakMap();
function getTargetFromInstanceHandle(
instanceHandle: mixed,
): ?ReactNativeElement {
// $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap.
const key: interface {} = instanceHandle;
return instanceHandleToTargetMap.get(key);
}
function setTargetForInstanceHandle(
instanceHandle: mixed,
target: ReactNativeElement,
): void {
// $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap.
const key: interface {} = instanceHandle;
instanceHandleToTargetMap.set(key, target);
}
function unsetTargetForInstanceHandle(instanceHandle: mixed): void {
// $FlowExpectedError[incompatible-type] instanceHandle is typed as mixed but we know it's an object and we need it to be to use it as a key in a WeakMap.
const key: interface {} = instanceHandle;
instanceHandleToTargetMap.delete(key);
}
// The mapping between ReactNativeElement and their corresponding shadow node
// also needs to be kept here because React removes the link when unmounting.
// We also keep the instance handle so we don't have to retrieve it again
// from the target to unobserve.
const targetToShadowNodeAndInstanceHandleMap: WeakMap<
ReactNativeElement,
[ReturnType<typeof getShadowNode>, mixed],
> = new WeakMap();
/**
* Registers the given intersection observer and returns a unique ID for it,
* which is required to start observing targets.
*/
export function registerObserver(
observer: IntersectionObserver,
callback: IntersectionObserverCallback,
): IntersectionObserverId {
const intersectionObserverId = nextIntersectionObserverId;
nextIntersectionObserverId++;
registeredIntersectionObservers.set(intersectionObserverId, {
observer,
callback,
});
return intersectionObserverId;
}
/**
* Unregisters the given intersection observer.
* This should only be called when an observer is no longer observing any
* targets.
*/
export function unregisterObserver(
intersectionObserverId: IntersectionObserverId,
): void {
const deleted = registeredIntersectionObservers.delete(
intersectionObserverId,
);
if (deleted && registeredIntersectionObservers.size === 0) {
NativeIntersectionObserver?.disconnect();
isConnected = false;
}
}
/**
* Starts observing a target on a specific intersection observer.
* If this is the first target being observed, this also sets up the centralized
* notification callback in native.
*/
export function observe({
intersectionObserverId,
target,
}: {
intersectionObserverId: IntersectionObserverId,
target: ReactNativeElement,
}): void {
if (NativeIntersectionObserver == null) {
warnNoNativeIntersectionObserver();
return;
}
const registeredObserver = registeredIntersectionObservers.get(
intersectionObserverId,
);
if (registeredObserver == null) {
console.error(
`IntersectionObserverManager: could not start observing target because IntersectionObserver with ID ${intersectionObserverId} was not registered.`,
);
return;
}
const targetShadowNode = getShadowNode(target);
if (targetShadowNode == null) {
console.error(
'IntersectionObserverManager: could not find reference to host node from target',
);
return;
}
const instanceHandle = getInstanceHandle(target);
if (instanceHandle == null) {
console.error(
'IntersectionObserverManager: could not find reference to instance handle from target',
);
return;
}
// Store the mapping between the instance handle and the target so we can
// access it even after the instance handle has been unmounted.
setTargetForInstanceHandle(instanceHandle, target);
// Same for the mapping between the target and its shadow node
// and instance handle.
targetToShadowNodeAndInstanceHandleMap.set(target, [
targetShadowNode,
instanceHandle,
]);
if (!isConnected) {
NativeIntersectionObserver.connect(notifyIntersectionObservers);
isConnected = true;
}
return NativeIntersectionObserver.observe({
intersectionObserverId,
targetShadowNode,
thresholds: registeredObserver.observer.thresholds,
});
}
export function unobserve(
intersectionObserverId: number,
target: ReactNativeElement,
): void {
if (NativeIntersectionObserver == null) {
warnNoNativeIntersectionObserver();
return;
}
const registeredObserver = registeredIntersectionObservers.get(
intersectionObserverId,
);
if (registeredObserver == null) {
console.error(
`IntersectionObserverManager: could not stop observing target because IntersectionObserver with ID ${intersectionObserverId} was not registered.`,
);
return;
}
const targetShadowNodeAndInstanceHandle =
targetToShadowNodeAndInstanceHandleMap.get(target);
if (targetShadowNodeAndInstanceHandle == null) {
console.error(
'IntersectionObserverManager: could not find registration data for target',
);
return;
}
const [targetShadowNode, instanceHandle] = targetShadowNodeAndInstanceHandle;
NativeIntersectionObserver.unobserve(
intersectionObserverId,
targetShadowNode,
);
// We can guarantee we won't receive any more entries for this target,
// so we don't need to keep the mappings anymore.
unsetTargetForInstanceHandle(instanceHandle);
targetToShadowNodeAndInstanceHandleMap.delete(target);
}
/**
* This function is called from native when there are `IntersectionObserver`
* entries to dispatch.
*/
function notifyIntersectionObservers(): void {
Systrace.beginEvent(
'IntersectionObserverManager.notifyIntersectionObservers',
);
try {
doNotifyIntersectionObservers();
} finally {
Systrace.endEvent();
}
}
function doNotifyIntersectionObservers(): void {
if (NativeIntersectionObserver == null) {
warnNoNativeIntersectionObserver();
return;
}
const nativeEntries = NativeIntersectionObserver.takeRecords();
const entriesByObserver: Map<
IntersectionObserverId,
Array<IntersectionObserverEntry>,
> = new Map();
for (const nativeEntry of nativeEntries) {
let list = entriesByObserver.get(nativeEntry.intersectionObserverId);
if (list == null) {
list = [];
entriesByObserver.set(nativeEntry.intersectionObserverId, list);
}
const target = getTargetFromInstanceHandle(
nativeEntry.targetInstanceHandle,
);
if (target == null) {
console.warn('Could not find target to create IntersectionObserverEntry');
continue;
}
list.push(createIntersectionObserverEntry(nativeEntry, target));
}
for (const [
intersectionObserverId,
entriesForObserver,
] of entriesByObserver) {
const registeredObserver = registeredIntersectionObservers.get(
intersectionObserverId,
);
if (!registeredObserver) {
// This could happen if the observer is disconnected between commit
// and mount. In this case, we can just ignore the entries.
return;
}
const {observer, callback} = registeredObserver;
try {
callback.call(observer, entriesForObserver, observer);
} catch (error) {
console.error(error);
}
}
}
function warnNoNativeIntersectionObserver() {
warnOnce(
'missing-native-intersection-observer',
'Missing native implementation of IntersectionObserver',
);
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "NativeIntersectionObserver.h"
#include <react/renderer/core/ShadowNode.h>
#include <react/renderer/uimanager/UIManagerBinding.h>
#include <react/renderer/uimanager/primitives.h>
#include "Plugins.h"
std::shared_ptr<facebook::react::TurboModule>
NativeIntersectionObserverModuleProvider(
std::shared_ptr<facebook::react::CallInvoker> jsInvoker) {
return std::make_shared<facebook::react::NativeIntersectionObserver>(
std::move(jsInvoker));
}
namespace facebook::react {
NativeIntersectionObserver::NativeIntersectionObserver(
std::shared_ptr<CallInvoker> jsInvoker)
: NativeIntersectionObserverCxxSpec(std::move(jsInvoker)) {}
void NativeIntersectionObserver::observe(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options) {
auto intersectionObserverId = options.intersectionObserverId;
auto shadowNode =
shadowNodeFromValue(runtime, std::move(options.targetShadowNode));
auto thresholds = options.thresholds;
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.observe(
intersectionObserverId, shadowNode, thresholds, uiManager);
}
void NativeIntersectionObserver::unobserve(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetShadowNode) {
auto shadowNode = shadowNodeFromValue(runtime, std::move(targetShadowNode));
intersectionObserverManager_.unobserve(intersectionObserverId, *shadowNode);
}
void NativeIntersectionObserver::connect(
jsi::Runtime& runtime,
AsyncCallback<> notifyIntersectionObserversCallback) {
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.connect(
uiManager, notifyIntersectionObserversCallback);
}
void NativeIntersectionObserver::disconnect(jsi::Runtime& runtime) {
auto& uiManager = getUIManagerFromRuntime(runtime);
intersectionObserverManager_.disconnect(uiManager);
}
std::vector<NativeIntersectionObserverEntry>
NativeIntersectionObserver::takeRecords(jsi::Runtime& runtime) {
auto entries = intersectionObserverManager_.takeRecords();
std::vector<NativeIntersectionObserverEntry> nativeModuleEntries;
nativeModuleEntries.reserve(entries.size());
for (const auto& entry : entries) {
nativeModuleEntries.emplace_back(
convertToNativeModuleEntry(entry, runtime));
}
return nativeModuleEntries;
}
NativeIntersectionObserverEntry
NativeIntersectionObserver::convertToNativeModuleEntry(
IntersectionObserverEntry entry,
jsi::Runtime& runtime) {
RectAsTuple targetRect = {
entry.targetRect.origin.x,
entry.targetRect.origin.y,
entry.targetRect.size.width,
entry.targetRect.size.height};
RectAsTuple rootRect = {
entry.rootRect.origin.x,
entry.rootRect.origin.y,
entry.rootRect.size.width,
entry.rootRect.size.height};
std::optional<RectAsTuple> intersectionRect;
if (entry.intersectionRect) {
intersectionRect = {
entry.intersectionRect.value().origin.x,
entry.intersectionRect.value().origin.y,
entry.intersectionRect.value().size.width,
entry.intersectionRect.value().size.height};
}
NativeIntersectionObserverEntry nativeModuleEntry = {
entry.intersectionObserverId,
(*entry.shadowNode).getInstanceHandle(runtime),
targetRect,
rootRect,
intersectionRect,
entry.isIntersectingAboveThresholds,
entry.time,
};
return nativeModuleEntry;
}
UIManager& NativeIntersectionObserver::getUIManagerFromRuntime(
jsi::Runtime& runtime) {
return UIManagerBinding::getBinding(runtime)->getUIManager();
}
} // namespace facebook::react

View File

@@ -0,0 +1,91 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#pragma once
#include <FBReactNativeSpec/FBReactNativeSpecJSI.h>
#include <react/renderer/observers/intersection/IntersectionObserverManager.h>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
namespace facebook::react {
using NativeIntersectionObserverIntersectionObserverId = int32_t;
using RectAsTuple = std::tuple<Float, Float, Float, Float>;
using NativeIntersectionObserverObserveOptions =
NativeIntersectionObserverCxxNativeIntersectionObserverObserveOptions<
// intersectionObserverId
NativeIntersectionObserverIntersectionObserverId,
// targetShadowNode
jsi::Object,
// thresholds
std::vector<Float>>;
template <>
struct Bridging<NativeIntersectionObserverObserveOptions>
: NativeIntersectionObserverCxxNativeIntersectionObserverObserveOptionsBridging<
NativeIntersectionObserverObserveOptions> {};
using NativeIntersectionObserverEntry =
NativeIntersectionObserverCxxNativeIntersectionObserverEntry<
// intersectionObserverId
NativeIntersectionObserverIntersectionObserverId,
// targetInstanceHandle
jsi::Value,
// targetRect
RectAsTuple,
// rootRect
RectAsTuple,
// intersectionRect
std::optional<RectAsTuple>,
// isIntersectingAboveThresholds
bool,
// time
double>;
template <>
struct Bridging<NativeIntersectionObserverEntry>
: NativeIntersectionObserverCxxNativeIntersectionObserverEntryBridging<
NativeIntersectionObserverEntry> {};
class NativeIntersectionObserver
: public NativeIntersectionObserverCxxSpec<NativeIntersectionObserver>,
std::enable_shared_from_this<NativeIntersectionObserver> {
public:
NativeIntersectionObserver(std::shared_ptr<CallInvoker> jsInvoker);
void observe(
jsi::Runtime& runtime,
NativeIntersectionObserverObserveOptions options);
void unobserve(
jsi::Runtime& runtime,
IntersectionObserverObserverId intersectionObserverId,
jsi::Object targetShadowNode);
void connect(
jsi::Runtime& runtime,
AsyncCallback<> notifyIntersectionObserversCallback);
void disconnect(jsi::Runtime& runtime);
std::vector<NativeIntersectionObserverEntry> takeRecords(
jsi::Runtime& runtime);
private:
IntersectionObserverManager intersectionObserverManager_{};
static UIManager& getUIManagerFromRuntime(jsi::Runtime& runtime);
static NativeIntersectionObserverEntry convertToNativeModuleEntry(
IntersectionObserverEntry entry,
jsi::Runtime& runtime);
};
} // namespace facebook::react

View File

@@ -0,0 +1,13 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
* @format
*/
export * from '../../src/private/specs/modules/NativeIntersectionObserver';
import NativeIntersectionObserver from '../../src/private/specs/modules/NativeIntersectionObserver';
export default NativeIntersectionObserver;

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/
import type ReactNativeElement from '../../../src/private/webapis/dom/nodes/ReactNativeElement';
import type IntersectionObserver from '../IntersectionObserver';
import type {
NativeIntersectionObserverEntry,
NativeIntersectionObserverObserveOptions,
Spec,
} from '../NativeIntersectionObserver';
import {getShadowNode} from '../../../src/private/webapis/dom/nodes/ReadOnlyNode';
import {getFabricUIManager} from '../../ReactNative/__mocks__/FabricUIManager';
import invariant from 'invariant';
import nullthrows from 'nullthrows';
type ObserverState = {
thresholds: $ReadOnlyArray<number>,
intersecting: boolean,
currentThreshold: ?number,
};
type Observation = {
...NativeIntersectionObserverObserveOptions,
state: ObserverState,
};
let pendingRecords: Array<NativeIntersectionObserverEntry> = [];
let callback: ?() => void;
let observations: Array<Observation> = [];
const FabricUIManagerMock = nullthrows(getFabricUIManager());
function createRecordFromObservation(
observation: Observation,
): NativeIntersectionObserverEntry {
return {
intersectionObserverId: observation.intersectionObserverId,
targetInstanceHandle: FabricUIManagerMock.__getInstanceHandleFromNode(
// $FlowExpectedError[incompatible-call]
observation.targetShadowNode,
),
targetRect: observation.state.intersecting ? [0, 0, 1, 1] : [20, 20, 1, 1],
rootRect: [0, 0, 10, 10],
intersectionRect: observation.state.intersecting ? [0, 0, 1, 1] : null,
isIntersectingAboveThresholds: observation.state.intersecting,
time: performance.now(),
};
}
function notifyIntersectionObservers(): void {
callback?.();
}
const NativeIntersectionObserverMock = {
observe: (options: NativeIntersectionObserverObserveOptions): void => {
invariant(
observations.find(
observation =>
observation.intersectionObserverId ===
options.intersectionObserverId &&
observation.targetShadowNode === options.targetShadowNode,
) == null,
'unexpected duplicate call to observe',
);
const observation = {
...options,
state: {
thresholds: options.thresholds,
intersecting: false,
currentThreshold: null,
},
};
observations.push(observation);
pendingRecords.push(createRecordFromObservation(observation));
setImmediate(notifyIntersectionObservers);
},
unobserve: (
intersectionObserverId: number,
targetShadowNode: mixed,
): void => {
const observationIndex = observations.findIndex(
observation =>
observation.intersectionObserverId === intersectionObserverId &&
observation.targetShadowNode === targetShadowNode,
);
invariant(
observationIndex !== -1,
'unexpected duplicate call to unobserve',
);
observations.splice(observationIndex, 1);
pendingRecords = pendingRecords.filter(
record =>
record.intersectionObserverId !== intersectionObserverId ||
record.targetInstanceHandle !==
FabricUIManagerMock.__getInstanceHandleFromNode(
// $FlowExpectedError[incompatible-call]
targetShadowNode,
),
);
},
connect: (notifyIntersectionObserversCallback: () => void): void => {
invariant(callback == null, 'unexpected call to connect');
invariant(
notifyIntersectionObserversCallback != null,
'unexpected null notify intersection observers callback',
);
callback = notifyIntersectionObserversCallback;
},
disconnect: (): void => {
invariant(callback != null, 'unexpected call to disconnect');
callback = null;
},
takeRecords: (): $ReadOnlyArray<NativeIntersectionObserverEntry> => {
const currentRecords = pendingRecords;
pendingRecords = [];
return currentRecords;
},
__forceTransitionForTests: (
observer: IntersectionObserver,
target: ReactNativeElement,
) => {
const targetShadowNode = getShadowNode(target);
const observation = observations.find(
obs =>
obs.intersectionObserverId === observer.__getObserverID() &&
obs.targetShadowNode === targetShadowNode,
);
invariant(
observation != null,
'cannot force transition on an unobserved target',
);
if (observation.state.intersecting) {
observation.state.intersecting = false;
observation.state.currentThreshold = null;
} else {
observation.state.intersecting = true;
observation.state.currentThreshold = observation.thresholds[0];
}
pendingRecords.push(createRecordFromObservation(observation));
setImmediate(notifyIntersectionObservers);
},
__getObservationsForTests: (
observer: IntersectionObserver,
): Array<{targetShadowNode: mixed, thresholds: $ReadOnlyArray<number>}> => {
const intersectionObserverId = observer.__getObserverID();
return observations
.filter(
observation =>
observation.intersectionObserverId === intersectionObserverId,
)
.map(observation => ({
targetShadowNode: observation.targetShadowNode,
thresholds: observation.thresholds,
}));
},
__isConnected: (): boolean => {
return callback != null;
},
};
(NativeIntersectionObserverMock: Spec);
export default NativeIntersectionObserverMock;