MM-55621 fix layout for tablets and foldables (#7697)
* MM-55621 fix layout for tablets and foldables * feedback review
This commit is contained in:
parent
d469470592
commit
de57c343f0
28 changed files with 228 additions and 125 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
|
|
|||
54
app/context/device/index.test.tsx
Normal file
54
app/context/device/index.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
37
app/context/device/index.tsx
Normal file
37
app/context/device/index.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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])));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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([]),
|
||||
|
|
|
|||
4
types/global/device.d.ts
vendored
4
types/global/device.d.ts
vendored
|
|
@ -2,6 +2,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
type SplitViewResult = {
|
||||
isSplitView?: boolean;
|
||||
isTablet?: boolean;
|
||||
isSplitView: boolean;
|
||||
isTablet: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue