From de57c343f0bde81fa3fd5ec359226b920ea6037b Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Tue, 5 Dec 2023 11:08:43 +0800 Subject: [PATCH] MM-55621 fix layout for tablets and foldables (#7697) * MM-55621 fix layout for tablets and foldables * feedback review --- android/app/build.gradle | 1 + .../com/mattermost/rnbeta/FoldableObserver.kt | 62 ++++++++++++++----- .../com/mattermost/rnbeta/MainActivity.java | 2 +- .../com/mattermost/rnbeta/SplitViewModule.kt | 20 +++--- app/actions/local/channel.ts | 2 +- app/actions/local/thread.ts | 4 +- app/actions/remote/channel.ts | 4 +- app/actions/remote/entry/common.ts | 2 +- app/actions/remote/team.ts | 4 +- app/actions/websocket/index.ts | 4 +- app/actions/websocket/posts.ts | 2 +- .../opengraph/opengraph_image/index.tsx | 5 +- .../threads_button.test.tsx.snap | 18 +++--- app/constants/device.ts | 5 +- app/context/device/index.test.tsx | 54 ++++++++++++++++ app/context/device/index.tsx | 37 +++++++++++ app/database/components/index.tsx | 17 ++--- app/hooks/device.ts | 32 ++-------- app/init/push_notifications.ts | 2 +- app/managers/analytics.ts | 4 +- .../calls/components/audio_device_button.tsx | 4 +- .../calls/screens/call_screen/call_screen.tsx | 2 +- app/screens/navigation.ts | 24 +++---- app/utils/helpers.ts | 11 +--- ios/Mattermost/Modules/SplitViewModule.m | 4 +- ios/Mattermost/Modules/SplitViewModule.swift | 21 ++++--- test/setup.ts | 2 +- types/global/device.d.ts | 4 +- 28 files changed, 228 insertions(+), 125 deletions(-) create mode 100644 app/context/device/index.test.tsx create mode 100644 app/context/device/index.tsx diff --git a/android/app/build.gradle b/android/app/build.gradle index b156b53c5..e066d860f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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' diff --git a/android/app/src/main/java/com/mattermost/rnbeta/FoldableObserver.kt b/android/app/src/main/java/com/mattermost/rnbeta/FoldableObserver.kt index d3019702f..c51a7df38 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/FoldableObserver.kt +++ b/android/app/src/main/java/com/mattermost/rnbeta/FoldableObserver.kt @@ -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 + 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() - .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() + .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 diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java index e5540ea5d..3f0efc13e 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.java @@ -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() { diff --git a/android/app/src/main/java/com/mattermost/rnbeta/SplitViewModule.kt b/android/app/src/main/java/com/mattermost/rnbeta/SplitViewModule.kt index eb53e60ca..89a99fdea 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/SplitViewModule.kt +++ b/android/app/src/main/java/com/mattermost/rnbeta/SplitViewModule.kt @@ -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 diff --git a/app/actions/local/channel.ts b/app/actions/local/channel.ts index 799438ac8..c2efcedcf 100644 --- a/app/actions/local/channel.ts +++ b/app/actions/local/channel.ts @@ -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); diff --git a/app/actions/local/thread.ts b/app/actions/local/thread.ts index 1071af575..76b5344cf 100644 --- a/app/actions/local/thread.ts +++ b/app/actions/local/thread.ts @@ -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(); diff --git a/app/actions/remote/channel.ts b/app/actions/remote/channel.ts index 4eda3e3bf..d43c4f799 100644 --- a/app/actions/remote/channel.ts +++ b/app/actions/remote/channel.ts @@ -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); diff --git a/app/actions/remote/entry/common.ts b/app/actions/remote/entry/common.ts index 9e09b1058..c8a394410 100644 --- a/app/actions/remote/entry/common.ts +++ b/app/actions/remote/entry/common.ts @@ -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 diff --git a/app/actions/remote/team.ts b/app/actions/remote/team.ts index 06de9530f..40076b485 100644 --- a/app/actions/remote/team.ts +++ b/app/actions/remote/team.ts @@ -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); diff --git a/app/actions/websocket/index.ts b/app/actions/websocket/index.ts index 95e1084a6..2a8f1f297 100644 --- a/app/actions/websocket/index.ts +++ b/app/actions/websocket/index.ts @@ -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, diff --git a/app/actions/websocket/posts.ts b/app/actions/websocket/posts.ts index 5557e4911..52e0f20bf 100644 --- a/app/actions/websocket/posts.ts +++ b/app/actions/websocket/posts.ts @@ -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; } diff --git a/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx b/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx index 5f022c95a..e72df172e 100644 --- a/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx +++ b/app/components/post_list/post/body/content/opengraph/opengraph_image/index.tsx @@ -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; }; diff --git a/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap b/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap index 8eb4071c9..f487a6ac2 100644 --- a/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap +++ b/app/components/threads_button/__snapshots__/threads_button.test.tsx.snap @@ -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, ] } diff --git a/app/constants/device.ts b/app/constants/device.ts index 2e06d7b86..fd168f365 100644 --- a/app/constants/device.ts +++ b/app/constants/device.ts @@ -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', }; diff --git a/app/context/device/index.test.tsx b/app/context/device/index.test.tsx new file mode 100644 index 000000000..23f3dc898 --- /dev/null +++ b/app/context/device/index.test.tsx @@ -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 ( + + {isSplitView} + {isTablet} + + ); +}; + +describe('', () => { + it('should match the initial value of the context', () => { + const {getByTestId} = render( + + + , + ); + + 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( + + + , + ); + + 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); + }); +}); diff --git a/app/context/device/index.tsx b/app/context/device/index.tsx new file mode 100644 index 000000000..018350185 --- /dev/null +++ b/app/context/device/index.tsx @@ -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 ( + + {children} + ); +}; + +export default DeviceInfoProvider; diff --git a/app/database/components/index.tsx b/app/database/components/index.tsx index 39dba96bb..047dbec92 100644 --- a/app/database/components/index.tsx +++ b/app/database/components/index.tsx @@ -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(Component: database={state.database} key={state.serverUrl} > - - - - - - - + + + + + + + + + ); }; diff --git a/app/hooks/device.ts b/app/hooks/device.ts index 1f3e0a5e0..3f5febe22 100644 --- a/app/hooks/device.ts +++ b/app/hooks/device.ts @@ -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) { diff --git a/app/init/push_notifications.ts b/app/init/push_notifications.ts index 1dcc332d7..517224c55 100644 --- a/app/init/push_notifications.ts +++ b/app/init/push_notifications.ts @@ -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); diff --git a/app/managers/analytics.ts b/app/managers/analytics.ts index 5f8c850fd..7e971cba1 100644 --- a/app/managers/analytics.ts +++ b/app/managers/analytics.ts @@ -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 = {}; @@ -50,7 +50,7 @@ export class Analytics { height, width, }, - isTablet: Device.IS_TABLET, + isTablet: isTablet(), os: DeviceInfo.getSystemVersion(), }, ip: '0.0.0.0', diff --git a/app/products/calls/components/audio_device_button.tsx b/app/products/calls/components/audio_device_button.tsx index a62fdbaf6..c550cceb7 100644 --- a/app/products/calls/components/audio_device_button.tsx +++ b/app/products/calls/components/audio_device_button.tsx @@ -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)], diff --git a/app/products/calls/screens/call_screen/call_screen.tsx b/app/products/calls/screens/call_screen/call_screen.tsx index acbaff5de..5c7180929 100644 --- a/app/products/calls/screens/call_screen/call_screen.tsx +++ b/app/products/calls/screens/call_screen/call_screen.tsx @@ -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)], diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index eabfdc751..fa412e197 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -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, diff --git a/app/utils/helpers.ts b/app/utils/helpers.ts index 4f996fffc..17417bd89 100644 --- a/app/utils/helpers.ts +++ b/app/utils/helpers.ts @@ -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]))); diff --git a/ios/Mattermost/Modules/SplitViewModule.m b/ios/Mattermost/Modules/SplitViewModule.m index 64b1256c8..6c062614b 100644 --- a/ios/Mattermost/Modules/SplitViewModule.m +++ b/ios/Mattermost/Modules/SplitViewModule.m @@ -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 diff --git a/ios/Mattermost/Modules/SplitViewModule.swift b/ios/Mattermost/Modules/SplitViewModule.swift index 0a4eacda1..f39a9693a 100644 --- a/ios/Mattermost/Modules/SplitViewModule.swift +++ b/ios/Mattermost/Modules/SplitViewModule.swift @@ -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 { + 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) diff --git a/test/setup.ts b/test/setup.ts index 2045c6131..eb39d8ba6 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -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([]), diff --git a/types/global/device.d.ts b/types/global/device.d.ts index 3409990cc..491099aac 100644 --- a/types/global/device.d.ts +++ b/types/global/device.d.ts @@ -2,6 +2,6 @@ // See LICENSE.txt for license information. type SplitViewResult = { - isSplitView?: boolean; - isTablet?: boolean; + isSplitView: boolean; + isTablet: boolean; }