stream-chat-react-native-core
Version:
The official React Native and Expo components for Stream Chat, a service for building chat applications
84 lines (74 loc) • 3.42 kB
text/typescript
import React from 'react';
import { Dimensions, Platform, View } from 'react-native';
import { EdgeInsets } from 'react-native-safe-area-context';
import { isRN86OrGreater } from '../../../utils/react-native-constants';
type MeasuredRect = { x: number; y: number; w: number; h: number };
/**
* On Android, `measureInWindow` historically did not account for the top inset, so we added it
* ourselves. React Native 0.86 fixed this upstream as part of its edge-to-edge rework and now
* returns topInset corrected window coordinates. On >= 0.86 adding it again double counts the
* inset and offsets every measured view down by `insets.top`, so we only compensate on Android
* running React Native below 0.86.
*/
const androidTopInsetCompensation = (insets: EdgeInsets): number =>
Platform.OS === 'android' && !isRN86OrGreater ? insets.top : 0;
/**
* How many screen lengths away from the origin a measured coordinate may fall before we treat it as
* bogus. Real targets (a view currently on screen) always measure well within one screen and the failure
* mode we guard against is off by tens of screens, so a generous multiplier cleanly separates the two
* with no risk of false positives on legitimate values.
*/
const SCREEN_BOUND_MULTIPLIER = 2;
/**
* `measureInWindow` can return wildly out of bounds coordinates on Android when the native window has
* been forced edge-to-edge behind React Native's back (i.e a library setting `FLAG_LAYOUT_NO_LIMITS`,
* as `react-native-system-navigation-bar` does for a transparent nav bar) while RN's own edge-to-edge
* (`edgeToEdgeEnabled`) is off. In that state the window relative measurement is corrupted and would
* return a position of the View far offscreen.
*/
export const isMeasuredRectBogus = (x: number, y: number, w: number, h: number): boolean => {
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h)) {
return true;
}
if (w <= 0 || h <= 0) {
return true;
}
const { width, height } = Dimensions.get('screen');
if (width <= 0 || height <= 0) {
// can't reason about bounds, trust the measurement
return false;
}
return (
Math.abs(x) > width * SCREEN_BOUND_MULTIPLIER || Math.abs(y) > height * SCREEN_BOUND_MULTIPLIER
);
};
export const measureInWindow = (
node: React.RefObject<View | null>,
insets: EdgeInsets,
): Promise<MeasuredRect> => {
return new Promise((resolve, reject) => {
const handle = node.current;
if (!handle) {
return reject(
new Error('The native handle could not be found while invoking measureInWindow.'),
);
}
handle.measureInWindow((x, y, w, h) => {
if (!isMeasuredRectBogus(x, y, w, h)) {
resolve({ h, w, x, y: y + androidTopInsetCompensation(insets) });
return;
}
// If `measureInWindow` returned an out of bounds rect, fallback to `measure()`, whose
// `pageX`/`pageY` are relative to the app root and are the same coordinate space as the window.
// They will stays correct when the window frame has been mutated out from under
// React Native.
if (typeof handle.measure !== 'function') {
resolve({ h, w, x, y: y + androidTopInsetCompensation(insets) });
return;
}
handle.measure((_x, _y, width, height, pageX, pageY) => {
resolve({ h: height, w: width, x: pageX, y: pageY });
});
});
});
};