MM-55621 fix layout for tablets and foldables (#7697)

* MM-55621 fix layout for tablets and foldables

* feedback review
This commit is contained in:
Elias Nahum 2023-12-05 11:08:43 +08:00 committed by GitHub
parent d469470592
commit de57c343f0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 228 additions and 125 deletions

View file

@ -206,6 +206,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
implementation 'androidx.window:window-core:1.1.0'
implementation 'androidx.window:window-rxjava3:1.0.0'
implementation 'androidx.window:window:1.0.0'
implementation 'androidx.appcompat:appcompat:1.6.1'

View file

@ -1,9 +1,13 @@
package com.mattermost.rnbeta
import android.app.Activity
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculator
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable
@ -12,6 +16,23 @@ import io.reactivex.rxjava3.disposables.Disposable
class FoldableObserver(private val activity: Activity) {
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
public var isDeviceFolded: Boolean = false
companion object {
private var instance: FoldableObserver? = null
fun getInstance(activity: Activity): FoldableObserver {
if (instance == null) {
instance = FoldableObserver(activity)
}
return instance!!
}
fun getInstance(): FoldableObserver? {
return instance
}
}
fun onCreate() {
observable = WindowInfoTracker.getOrCreate(activity)
@ -25,20 +46,8 @@ class FoldableObserver(private val activity: Activity) {
disposable = observable.observeOn(AndroidSchedulers.mainThread())
.subscribe { layoutInfo ->
val splitViewModule = SplitViewModule.getInstance()
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
when {
foldingFeature?.state === FoldingFeature.State.FLAT ->
splitViewModule?.setDeviceFolded(false)
isTableTopPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
isBookPosture(foldingFeature) ->
splitViewModule?.setDeviceFolded(false)
else -> {
splitViewModule?.setDeviceFolded(true)
}
}
setIsDeviceFolded(layoutInfo)
splitViewModule?.setDeviceFolded()
}
}
@ -46,6 +55,31 @@ class FoldableObserver(private val activity: Activity) {
disposable?.dispose()
}
private fun setIsDeviceFolded(layoutInfo: WindowLayoutInfo) {
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
isDeviceFolded = when {
foldingFeature === null -> isCompactView()
foldingFeature.state === FoldingFeature.State.FLAT -> false
isTableTopPosture(foldingFeature) -> false
isBookPosture(foldingFeature) -> false
else -> true
}
}
fun isCompactView(): Boolean {
val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
val width = metrics.bounds.width()
val height = metrics.bounds.height()
val density = activity.resources.displayMetrics.density
val windowSizeClass = WindowSizeClass.compute(width/density, height/density)
val widthWindowSizeClass = windowSizeClass.windowWidthSizeClass
val heightWindowSizeClass = windowSizeClass.windowHeightSizeClass
return widthWindowSizeClass === WindowWidthSizeClass.COMPACT || heightWindowSizeClass === WindowHeightSizeClass.COMPACT
}
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

View file

@ -18,7 +18,7 @@ import java.util.Objects;
public class MainActivity extends NavigationActivity {
private boolean HWKeyboardConnected = false;
private final FoldableObserver foldableObserver = new FoldableObserver(this);
private final FoldableObserver foldableObserver = FoldableObserver.Companion.getInstance(this);
@Override
protected String getMainComponentName() {

View file

@ -5,7 +5,6 @@ import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEm
import com.learnium.RNDeviceInfo.resolver.DeviceTypeResolver
class SplitViewModule(private var reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
private var isDeviceFolded: Boolean = false
private var listenerCount = 0
companion object {
@ -39,7 +38,11 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
if (currentActivity != null) {
val deviceResolver = DeviceTypeResolver(this.reactContext)
val map = Arguments.createMap()
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
var isSplitView = folded;
if (currentActivity?.isInMultiWindowMode == true) {
isSplitView = FoldableObserver.getInstance()?.isCompactView() == true
}
map.putBoolean("isSplitView", isSplitView)
map.putBoolean("isTablet", deviceResolver.isTablet)
return map
}
@ -47,17 +50,16 @@ class SplitViewModule(private var reactContext: ReactApplicationContext) : React
return null
}
fun setDeviceFolded(folded: Boolean) {
val map = getSplitViewResults(folded)
if (listenerCount > 0 && isDeviceFolded != folded) {
fun setDeviceFolded() {
val map = getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
if (listenerCount > 0) {
sendEvent(map)
}
isDeviceFolded = folded
}
@ReactMethod
fun isRunningInSplitView(promise: Promise) {
promise.resolve(getSplitViewResults(isDeviceFolded))
@ReactMethod(isBlockingSynchronousMethod = true)
fun isRunningInSplitView(): WritableMap? {
return getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
}
@ReactMethod

View file

@ -32,7 +32,7 @@ export async function switchToChannel(serverUrl: string, channelId: string, team
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
let models: Model[] = [];
const dt = Date.now();
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
const system = await getCommonSystemValues(database);
const member = await getMyChannel(database, channelId);

View file

@ -43,7 +43,7 @@ export const switchToGlobalThreads = async (serverUrl: string, teamId?: string,
await operator.batchRecords(models, 'switchToGlobalThreads');
}
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
if (isTabletDevice) {
DeviceEventEmitter.emit(Navigation.NAVIGATION_HOME, Screens.GLOBAL_THREADS);
} else {
@ -75,7 +75,7 @@ export const switchToThread = async (serverUrl: string, rootId: string, isFromNo
}
const currentTeamId = await getCurrentTeamId(database);
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
const teamId = channel.teamId || currentTeamId;
const currentThreadId = EphemeralStore.getCurrentThreadId();

View file

@ -295,7 +295,7 @@ export async function leaveChannel(serverUrl: string, channelId: string) {
const client = NetworkManager.getClient(serverUrl);
const {database, operator} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
const user = await getCurrentUser(database);
const models: Model[] = [];
@ -1239,7 +1239,7 @@ export const handleKickFromChannel = async (serverUrl: string, channelId: string
await dismissAllModalsAndPopToRoot();
}
const tabletDevice = await isTablet();
const tabletDevice = isTablet();
if (tabletDevice) {
const teamId = await getCurrentTeamId(database);

View file

@ -482,7 +482,7 @@ export async function handleEntryAfterLoadNavigation(
const mountedScreens = NavigationStore.getScreensInStack();
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
const isThreadsMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = await isTablet();
const tabletDevice = isTablet();
if (!currentTeamIdAfterLoad) {
// First load or no team

View file

@ -84,7 +84,7 @@ export async function addUserToTeam(serverUrl: string, teamId: string, userId: s
setTeamLoading(serverUrl, false);
loadEventSent = false;
if (await isTablet()) {
if (isTablet()) {
const channel = await getDefaultChannelForTeam(database, teamId);
if (channel) {
fetchPostsForChannel(serverUrl, channel.id);
@ -395,7 +395,7 @@ export async function handleTeamChange(serverUrl: string, teamId: string) {
let channelId = '';
DeviceEventEmitter.emit(Events.TEAM_SWITCH, true);
if (await isTablet()) {
if (isTablet()) {
channelId = await getNthLastChannelFromTeam(database, teamId);
if (channelId) {
await switchToChannelById(serverUrl, channelId, teamId);

View file

@ -168,7 +168,7 @@ async function doReconnect(serverUrl: string) {
await operator.batchRecords(models, 'doReconnect');
}
const tabletDevice = await isTablet();
const tabletDevice = isTablet();
if (tabletDevice && initialChannelId === currentChannelId) {
await markChannelAsRead(serverUrl, initialChannelId);
markChannelAsViewed(serverUrl, initialChannelId);
@ -479,7 +479,7 @@ async function fetchPostDataIfNeeded(serverUrl: string) {
const mountedScreens = NavigationStore.getScreensInStack();
const isChannelScreenMounted = mountedScreens.includes(Screens.CHANNEL);
const isThreadScreenMounted = mountedScreens.includes(Screens.THREAD);
const tabletDevice = await isTablet();
const tabletDevice = isTablet();
if (isCRTEnabled && isThreadScreenMounted) {
// Fetch new posts in the thread only when CRT is enabled,

View file

@ -130,7 +130,7 @@ export async function handleNewPostEvent(serverUrl: string, msg: WebSocketMessag
} else if ((post.channel_id === currentChannelId)) {
const isChannelScreenMounted = NavigationStore.getScreensInStack().includes(Screens.CHANNEL);
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
if (isChannelScreenMounted || isTabletDevice) {
markAsViewed = false;
}

View file

@ -6,12 +6,13 @@ import {TouchableWithoutFeedback, useWindowDimensions} from 'react-native';
import FastImage, {type Source} from 'react-native-fast-image';
import Animated from 'react-native-reanimated';
import {Device as DeviceConstant, View as ViewConstants} from '@constants';
import {View as ViewConstants} from '@constants';
import {GalleryInit} from '@context/gallery';
import {useGalleryItem} from '@hooks/gallery';
import {lookupMimeType} from '@utils/file';
import {openGalleryAtIndex} from '@utils/gallery';
import {generateId} from '@utils/general';
import {isTablet} from '@utils/helpers';
import {calculateDimensions} from '@utils/images';
import {type BestImage, getNearestPoint} from '@utils/opengraph';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@ -51,7 +52,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
const getViewPostWidth = (isReplyPost: boolean, deviceHeight: number, deviceWidth: number) => {
const deviceSize = deviceWidth > deviceHeight ? deviceHeight : deviceWidth;
const viewPortWidth = deviceSize - VIEWPORT_IMAGE_OFFSET - (isReplyPost ? VIEWPORT_IMAGE_REPLY_OFFSET : 0);
const tabletOffset = DeviceConstant.IS_TABLET ? ViewConstants.TABLET_SIDEBAR_WIDTH : 0;
const tabletOffset = isTablet() ? ViewConstants.TABLET_SIDEBAR_WIDTH : 0;
return viewPortWidth - tabletOffset;
};

View file

@ -44,8 +44,8 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"flexDirection": "row",
},
false,
false,
false,
undefined,
undefined,
{
"minHeight": 40,
},
@ -82,7 +82,7 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"color": "rgba(255,255,255,0.72)",
},
false,
false,
undefined,
undefined,
]
}
@ -137,8 +137,8 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"flexDirection": "row",
},
false,
false,
false,
undefined,
undefined,
{
"minHeight": 40,
},
@ -177,7 +177,7 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"color": "rgba(255,255,255,0.72)",
},
false,
false,
undefined,
{
"color": "#3f4350",
},
@ -236,8 +236,8 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"flexDirection": "row",
},
false,
false,
false,
undefined,
undefined,
{
"minHeight": 40,
},
@ -274,7 +274,7 @@ exports[`Thread item in the channel list Threads Component should match snapshot
"color": "rgba(255,255,255,0.72)",
},
false,
false,
undefined,
undefined,
]
}

View file

@ -1,12 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DeviceInfo from 'react-native-device-info';
import FileSystem from 'react-native-fs';
import {isTablet} from '@utils/helpers';
export default {
DOCUMENTS_PATH: `${FileSystem.CachesDirectoryPath}/Documents`,
IS_TABLET: DeviceInfo.isTablet(),
IS_TABLET: isTablet(),
PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn',
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
};

View file

@ -0,0 +1,54 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useContext} from 'react';
import {NativeEventEmitter, NativeModules, Text, View} from 'react-native';
import DeviceInfoProvider, {DeviceContext} from '@context/device';
import {act, render} from '@test/intl-test-helper';
const {SplitView} = NativeModules;
const emitter = new NativeEventEmitter(SplitView);
const TestComponent = () => {
const {isSplitView, isTablet} = useContext(DeviceContext);
return (
<View>
<Text testID='isSplitView'>{isSplitView}</Text>
<Text testID='isTablet'>{isTablet}</Text>
</View>
);
};
describe('<DeviceInfoProvider/>', () => {
it('should match the initial value of the context', () => {
const {getByTestId} = render(
<DeviceInfoProvider>
<TestComponent/>
</DeviceInfoProvider>,
);
const isTablet = getByTestId('isTablet');
const isSplitView = getByTestId('isSplitView');
expect(isTablet.props.children).toBe(false);
expect(isSplitView.props.children).toBe(false);
});
it('should match the value emitted of the context', () => {
const {getByTestId} = render(
<DeviceInfoProvider>
<TestComponent/>
</DeviceInfoProvider>,
);
act(() => {
emitter.emit('SplitViewChanged', {isTablet: true, isSplitView: true});
});
const isTablet = getByTestId('isTablet');
const isSplitView = getByTestId('isSplitView');
expect(isTablet.props.children).toBe(true);
expect(isSplitView.props.children).toBe(true);
});
});

View file

@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {createContext, useEffect, useState} from 'react';
import {NativeEventEmitter, NativeModules} from 'react-native';
type Props = {
children: React.ReactNode;
}
const {SplitView} = NativeModules;
const {isRunningInSplitView} = SplitView;
const emitter = new NativeEventEmitter(SplitView);
export let info: SplitViewResult = isRunningInSplitView();
export const DeviceContext = createContext(info);
const {Provider} = DeviceContext;
const DeviceInfoProvider = ({children}: Props) => {
const [deviceInfo, setDeviceInfo] = useState(info);
useEffect(() => {
const listener = emitter.addListener('SplitViewChanged', (result: SplitViewResult) => {
setDeviceInfo(result);
info = result;
});
return () => listener.remove();
}, []);
return (
<Provider value={deviceInfo}>
{children}
</Provider>);
};
export default DeviceInfoProvider;

View file

@ -4,6 +4,7 @@
import {DatabaseProvider} from '@nozbe/watermelondb/react';
import React, {type ComponentType, useEffect, useState} from 'react';
import DeviceInfoProvider from '@context/device';
import ServerProvider from '@context/server';
import ThemeProvider from '@context/theme';
import UserLocaleProvider from '@context/user_locale';
@ -61,13 +62,15 @@ export function withServerDatabase<T extends JSX.IntrinsicAttributes>(Component:
database={state.database}
key={state.serverUrl}
>
<UserLocaleProvider database={state.database}>
<ServerProvider server={{displayName: state.serverDisplayName, url: state.serverUrl}}>
<ThemeProvider database={state.database}>
<Component {...props}/>
</ThemeProvider>
</ServerProvider>
</UserLocaleProvider>
<DeviceInfoProvider>
<UserLocaleProvider database={state.database}>
<ServerProvider server={{displayName: state.serverDisplayName, url: state.serverUrl}}>
<ThemeProvider database={state.database}>
<Component {...props}/>
</ThemeProvider>
</ServerProvider>
</UserLocaleProvider>
</DeviceInfoProvider>
</DatabaseProvider>
);
};

View file

@ -2,37 +2,15 @@
// See LICENSE.txt for license information.
import React, {type RefObject, useEffect, useRef, useState} from 'react';
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, useWindowDimensions, View} from 'react-native';
import {AppState, Keyboard, Platform, useWindowDimensions, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Device} from '@constants';
import {DeviceContext} from '@context/device';
import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
const {SplitView} = NativeModules;
const {isRunningInSplitView} = SplitView;
const emitter = new NativeEventEmitter(SplitView);
export function useSplitView() {
const [isSplitView, setIsSplitView] = useState(false);
useEffect(() => {
if (Device.IS_TABLET) {
isRunningInSplitView().then((result: SplitViewResult) => {
if (result.isSplitView != null) {
setIsSplitView(result.isSplitView);
}
});
}
const listener = emitter.addListener('SplitViewChanged', (result: SplitViewResult) => {
if (result.isSplitView != null) {
setIsSplitView(result.isSplitView);
}
});
return () => listener.remove();
}, []);
const {isSplitView} = React.useContext(DeviceContext);
return isSplitView;
}
@ -51,8 +29,8 @@ export function useAppState() {
}
export function useIsTablet() {
const isSplitView = useSplitView();
return Device.IS_TABLET && !isSplitView;
const {isSplitView, isTablet} = React.useContext(DeviceContext);
return isTablet && !isSplitView;
}
export function useKeyboardHeightWithDuration(keyboardTracker?: React.RefObject<KeyboardTrackingViewRef>) {

View file

@ -119,7 +119,7 @@ class PushNotifications {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (database) {
const isTabletDevice = await isTablet();
const isTabletDevice = isTablet();
const displayName = await getServerDisplayName(serverUrl);
const channelId = await getCurrentChannelId(database);
const isCRTEnabled = await getIsCRTEnabled(database);

View file

@ -5,7 +5,7 @@ import {Dimensions} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import LocalConfig from '@assets/config.json';
import {Device} from '@constants';
import {isTablet} from '@utils/helpers';
import {isSystemAdmin} from '@utils/user';
const clientMap: Record<string, Analytics> = {};
@ -50,7 +50,7 @@ export class Analytics {
height,
width,
},
isTablet: Device.IS_TABLET,
isTablet: isTablet(),
os: DeviceInfo.getSystemVersion(),
},
ip: '0.0.0.0',

View file

@ -43,7 +43,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c
const bluetoothLabel = intl.formatMessage({id: 'mobile.calls_bluetooth', defaultMessage: 'Bluetooth'});
const headsetLabel = intl.formatMessage({id: 'mobile.calls_headset', defaultMessage: 'Headset'});
const deviceSelector = useCallback(async () => {
const deviceSelector = useCallback(() => {
const currentDevice = audioDeviceInfo.selectedAudioDevice;
let available = audioDeviceInfo.availableAudioDeviceList;
if (available.includes(AudioDevice.WiredHeadset)) {
@ -106,7 +106,7 @@ export const AudioDeviceButton = ({pressableStyle, iconStyle, buttonTextStyle, c
);
};
await bottomSheet({
bottomSheet({
closeButtonId: 'close-other-actions',
renderContent,
snapPoints: [1, bottomSheetSnapPoint(available.length + 1, ITEM_HEIGHT, bottom)],

View file

@ -504,7 +504,7 @@ const CallScreen = ({
};
const items = isHost && EnableRecordings ? 3 : 2;
await bottomSheet({
bottomSheet({
closeButtonId: 'close-other-actions',
renderContent,
snapPoints: [1, bottomSheetSnapPoint(items, ITEM_HEIGHT, bottom)],

View file

@ -4,16 +4,17 @@
/* eslint-disable max-lines */
import merge from 'deepmerge';
import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert, type EmitterSubscription} from 'react-native';
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription} from 'react-native';
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
import tinyColor from 'tinycolor2';
import CompassIcon from '@components/compass_icon';
import {Device, Events, Screens, Launch} from '@constants';
import {Events, Screens, Launch} from '@constants';
import {NOT_READY} from '@constants/screens';
import {getDefaultThemeByAppearance} from '@context/theme';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import {isTablet} from '@utils/helpers';
import {logError} from '@utils/log';
import {appearanceControlledScreens, mergeNavigationOptions} from '@utils/navigation';
import {changeOpacity, setNavigatorStyles} from '@utils/theme';
@ -22,9 +23,6 @@ import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
import type {LaunchProps} from '@typings/launch';
import type {AvailableScreens, NavButtons} from '@typings/screens/navigation';
const {SplitView} = NativeModules;
const {isRunningInSplitView} = SplitView;
const alpha = {
from: 0,
to: 1,
@ -193,7 +191,7 @@ Navigation.setDefaultOptions({
},
},
layout: {
orientation: Device.IS_TABLET ? allOrientations : portraitOrientation,
orientation: isTablet() ? allOrientations : portraitOrientation,
},
topBar: {
title: {
@ -782,11 +780,8 @@ type BottomSheetArgs = {
title: string;
}
export async function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId}: BottomSheetArgs) {
const {isSplitView} = await isRunningInSplitView();
const isTablet = Device.IS_TABLET && !isSplitView;
if (isTablet) {
export function bottomSheet({title, renderContent, footerComponent, snapPoints, initialSnapIndex = 1, theme, closeButtonId}: BottomSheetArgs) {
if (isTablet()) {
showModal(Screens.BOTTOM_SHEET, title, {
closeButtonId,
initialSnapIndex,
@ -817,11 +812,8 @@ type AsBottomSheetArgs = {
title: string;
}
export async function openAsBottomSheet({closeButtonId, screen, theme, title, props}: AsBottomSheetArgs) {
const {isSplitView} = await isRunningInSplitView();
const isTablet = Device.IS_TABLET && !isSplitView;
if (isTablet) {
export function openAsBottomSheet({closeButtonId, screen, theme, title, props}: AsBottomSheetArgs) {
if (isTablet()) {
showModal(screen, title, {
closeButtonId,
...props,

View file

@ -4,7 +4,6 @@
import moment, {type Moment} from 'moment-timezone';
import {NativeModules, Platform} from 'react-native';
import {Device} from '@constants';
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
import {STATUS_BAR_HEIGHT} from '@constants/view';
@ -130,13 +129,9 @@ export function getRoundedTime(value: Moment) {
return start.add(remainder, 'm').seconds(0).milliseconds(0);
}
export async function isTablet() {
if (Device.IS_TABLET) {
const {isSplitView} = await isRunningInSplitView();
return !isSplitView;
}
return false;
export function isTablet() {
const result: SplitViewResult = isRunningInSplitView();
return result.isTablet && !result.isSplitView;
}
export const pluckUnique = (key: string) => (array: Array<{[key: string]: unknown}>) => Array.from(new Set(array.map((obj) => obj[key])));

View file

@ -5,9 +5,7 @@
RCT_EXTERN_METHOD(supportedEvents)
RCT_EXTERN_METHOD(startObserving)
RCT_EXTERN_METHOD(stopObserving)
RCT_EXTERN_METHOD(isRunningInSplitView:
(RCTPromiseResolveBlock)resolve
withRejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD(isRunningInSplitView)
RCT_EXTERN_METHOD(unlockOrientation)
RCT_EXTERN_METHOD(lockPortrait)
@end

View file

@ -47,14 +47,21 @@ class SplitViewModule: RCTEventEmitter {
}
}
@objc(isRunningInSplitView:withRejecter:)
func isRunningInSplitView(resolve:@escaping RCTPromiseResolveBlock, reject:RCTPromiseRejectBlock) -> Void {
DispatchQueue.main.async { [weak self] in
resolve([
"isSplitView": !(self?.isRunningInFullScreen() ?? false),
"isTablet": UIDevice.current.userInterfaceIdiom == .pad,
])
@objc(isRunningInSplitView)
func isRunningInSplitView() -> Dictionary<String, Bool> {
let queue = DispatchQueue.main
let group = DispatchGroup()
var shouldBeConsideredFullScreen = true
group.enter()
queue.async(group: group) { [weak self] in
shouldBeConsideredFullScreen = self?.isRunningInFullScreen() ?? true
group.leave()
}
group.wait()
return [
"isSplitView": !shouldBeConsideredFullScreen,
"isTablet": UIDevice.current.userInterfaceIdiom == .pad,
]
}
@objc(unlockOrientation)

View file

@ -109,7 +109,7 @@ jest.doMock('react-native', () => {
SplitView: {
addListener: jest.fn(),
removeListeners: jest.fn(),
isRunningInSplitView: jest.fn().mockResolvedValue(() => ({isSplitView: false, isTablet: false})),
isRunningInSplitView: jest.fn().mockReturnValue(() => ({isSplitView: false, isTablet: false})),
},
Notifications: {
getDeliveredNotifications: jest.fn().mockResolvedValue([]),

View file

@ -2,6 +2,6 @@
// See LICENSE.txt for license information.
type SplitViewResult = {
isSplitView?: boolean;
isTablet?: boolean;
isSplitView: boolean;
isTablet: boolean;
}