forked from Ivasoft/mattermost-mobile
Support for Android Tablets & Foldable (#7025)
* Add Support for Android tablets & foldables * add tablet and book posture * Regenerate disposed observable on WindowInfoTracker
This commit is contained in:
@@ -206,6 +206,10 @@ dependencies {
|
||||
}
|
||||
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2'
|
||||
implementation 'io.reactivex.rxjava3:rxjava:3.1.6'
|
||||
implementation 'io.reactivex.rxjava3:rxandroid:3.0.2'
|
||||
implementation 'androidx.window:window-rxjava3:1.0.0'
|
||||
implementation 'androidx.window:window:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.1'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.window.layout.FoldingFeature
|
||||
import androidx.window.layout.WindowInfoTracker
|
||||
import androidx.window.layout.WindowLayoutInfo
|
||||
import androidx.window.rxjava3.layout.windowLayoutInfoObservable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
|
||||
class FoldableObserver(private val activity: Activity) {
|
||||
private var disposable: Disposable? = null
|
||||
private lateinit var observable: Observable<WindowLayoutInfo>
|
||||
|
||||
public fun onCreate() {
|
||||
observable = WindowInfoTracker.getOrCreate(activity)
|
||||
.windowLayoutInfoObservable(activity)
|
||||
}
|
||||
|
||||
public fun onStart() {
|
||||
if (disposable?.isDisposed == true) {
|
||||
onCreate()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun onStop() {
|
||||
disposable?.dispose()
|
||||
}
|
||||
|
||||
private fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
|
||||
}
|
||||
|
||||
private fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
|
||||
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
|
||||
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,9 @@ import com.github.emilioicai.hwkeyboardevent.HWKeyboardEventModule;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
|
||||
public class MainActivity extends NavigationActivity {
|
||||
private boolean HWKeyboardConnected = false;
|
||||
private FoldableObserver foldableObserver = new FoldableObserver(this);
|
||||
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
@@ -43,6 +43,19 @@ public class MainActivity extends NavigationActivity {
|
||||
super.onCreate(null);
|
||||
setContentView(R.layout.launch_screen);
|
||||
setHWKeyboardConnected();
|
||||
foldableObserver.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
foldableObserver.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
foldableObserver.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.mattermost.rnbeta;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -42,7 +44,6 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
public static MainApplication instance;
|
||||
|
||||
public Boolean sharedExtensionIsOpened = false;
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
@@ -69,6 +70,8 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
return ShareModule.getInstance(reactContext);
|
||||
case "Notifications":
|
||||
return NotificationsModule.getInstance(instance, reactContext);
|
||||
case "SplitView":
|
||||
return SplitViewModule.Companion.getInstance(reactContext);
|
||||
default:
|
||||
throw new IllegalArgumentException("Could not find module " + name);
|
||||
}
|
||||
@@ -81,6 +84,7 @@ public class MainApplication extends NavigationApplication implements INotificat
|
||||
map.put("MattermostManaged", new ReactModuleInfo("MattermostManaged", "com.mattermost.rnbeta.MattermostManagedModule", false, false, false, false, false));
|
||||
map.put("MattermostShare", new ReactModuleInfo("MattermostShare", "com.mattermost.share.ShareModule", false, false, true, false, false));
|
||||
map.put("Notifications", new ReactModuleInfo("Notifications", "com.mattermost.rnbeta.NotificationsModule", false, false, false, false, false));
|
||||
map.put("SplitView", new ReactModuleInfo("SplitView", "com.mattermost.rnbeta.SplitViewModule", false, false, false, false, false));
|
||||
return map;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -133,19 +133,6 @@ public class MattermostManagedModule extends ReactContextBaseJavaModule {
|
||||
promise.resolve(map);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void isRunningInSplitView(final Promise promise) {
|
||||
WritableMap result = Arguments.createMap();
|
||||
Activity current = getCurrentActivity();
|
||||
if (current != null) {
|
||||
result.putBoolean("isSplitView", current.isInMultiWindowMode());
|
||||
} else {
|
||||
result.putBoolean("isSplitView", false);
|
||||
}
|
||||
|
||||
promise.resolve(result);
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void saveFile(String path, final Promise promise) {
|
||||
Uri contentUri;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.mattermost.rnbeta
|
||||
|
||||
import com.facebook.react.bridge.*
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
|
||||
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 {
|
||||
private var instance: SplitViewModule? = null
|
||||
|
||||
fun getInstance(reactContext: ReactApplicationContext): SplitViewModule {
|
||||
if (instance == null) {
|
||||
instance = SplitViewModule(reactContext)
|
||||
} else {
|
||||
instance!!.reactContext = reactContext
|
||||
}
|
||||
|
||||
return instance!!
|
||||
}
|
||||
|
||||
fun getInstance(): SplitViewModule? {
|
||||
return instance
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun getName() = "SplitView"
|
||||
|
||||
fun sendEvent(eventName: String,
|
||||
params: WritableMap?) {
|
||||
reactContext
|
||||
.getJSModule(RCTDeviceEventEmitter::class.java)
|
||||
.emit(eventName, params)
|
||||
}
|
||||
|
||||
private fun getSplitViewResults(folded: Boolean) : WritableMap? {
|
||||
if (currentActivity != null) {
|
||||
val deviceResolver = DeviceTypeResolver(this.reactContext)
|
||||
val map = Arguments.createMap()
|
||||
map.putBoolean("isSplitView", currentActivity!!.isInMultiWindowMode || folded)
|
||||
map.putBoolean("isTablet", deviceResolver.isTablet)
|
||||
return map
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun setDeviceFolded(folded: Boolean) {
|
||||
val map = getSplitViewResults(folded)
|
||||
if (listenerCount > 0 && isDeviceFolded != folded) {
|
||||
sendEvent("SplitViewChanged", map)
|
||||
}
|
||||
isDeviceFolded = folded
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun isRunningInSplitView(promise: Promise) {
|
||||
promise.resolve(getSplitViewResults(isDeviceFolded))
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun addListener(eventName: String) {
|
||||
listenerCount += 1
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
fun removeListeners(count: Int) {
|
||||
listenerCount -= count
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Platform} from 'react-native';
|
||||
import DeviceInfo from 'react-native-device-info';
|
||||
import FileSystem from 'react-native-fs';
|
||||
|
||||
export default {
|
||||
DOCUMENTS_PATH: `${FileSystem.CachesDirectoryPath}/Documents`,
|
||||
IS_TABLET: Platform.select({android: false, default: DeviceInfo.isTablet()}),
|
||||
IS_TABLET: DeviceInfo.isTablet(),
|
||||
PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn',
|
||||
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
|
||||
};
|
||||
|
||||
@@ -2,27 +2,36 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {RefObject, useEffect, useRef, useState} from 'react';
|
||||
import {AppState, Keyboard, NativeModules, Platform, useWindowDimensions, View} from 'react-native';
|
||||
import {AppState, Keyboard, NativeEventEmitter, NativeModules, Platform, View} from 'react-native';
|
||||
import {useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {Device} from '@constants';
|
||||
|
||||
import type {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
const {SplitView} = NativeModules;
|
||||
const {isRunningInSplitView} = SplitView;
|
||||
const emitter = new NativeEventEmitter(SplitView);
|
||||
|
||||
export function useSplitView() {
|
||||
const [isSplitView, setIsSplitView] = useState(false);
|
||||
const dimensions = useWindowDimensions();
|
||||
|
||||
useEffect(() => {
|
||||
if (Device.IS_TABLET) {
|
||||
isRunningInSplitView().then((result: {isSplitView: boolean}) => {
|
||||
setIsSplitView(result.isSplitView);
|
||||
isRunningInSplitView().then((result: SplitViewResult) => {
|
||||
if (result.isSplitView != null) {
|
||||
setIsSplitView(result.isSplitView);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [dimensions]);
|
||||
const listener = emitter.addListener('SplitViewChanged', (result: SplitViewResult) => {
|
||||
if (result.isSplitView != null) {
|
||||
setIsSplitView(result.isSplitView);
|
||||
}
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
return isSplitView;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Alert, DeviceEventEmitter, Linking} from 'react-native';
|
||||
import {Alert, DeviceEventEmitter, Linking, NativeEventEmitter, NativeModules} from 'react-native';
|
||||
import RNLocalize from 'react-native-localize';
|
||||
import semver from 'semver';
|
||||
|
||||
import {switchToChannelById} from '@actions/remote/channel';
|
||||
import {autoUpdateTimezone} from '@actions/remote/user';
|
||||
import LocalConfig from '@assets/config.json';
|
||||
import {Events, Sso} from '@constants';
|
||||
import {Device, Events, Sso} from '@constants';
|
||||
import {MIN_REQUIRED_VERSION} from '@constants/supported_server';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||
import {getServerCredentials} from '@init/credentials';
|
||||
import * as analytics from '@managers/analytics';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {getAllServers, getActiveServerUrl} from '@queries/app/servers';
|
||||
import {queryTeamDefaultChannel} from '@queries/servers/channel';
|
||||
import {getCommonSystemValues} from '@queries/servers/system';
|
||||
import {getTeamChannelHistory} from '@queries/servers/team';
|
||||
import {setScreensOrientation} from '@screens/navigation';
|
||||
import {handleDeepLink} from '@utils/deep_link';
|
||||
import {logError} from '@utils/log';
|
||||
|
||||
@@ -20,6 +26,9 @@ import type {jsAndNativeErrorHandler} from '@typings/global/error_handling';
|
||||
|
||||
type LinkingCallbackArg = {url: string};
|
||||
|
||||
const {SplitView} = NativeModules;
|
||||
const splitViewEmitter = new NativeEventEmitter(SplitView);
|
||||
|
||||
class GlobalEventHandler {
|
||||
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
|
||||
|
||||
@@ -39,6 +48,7 @@ class GlobalEventHandler {
|
||||
}
|
||||
});
|
||||
|
||||
splitViewEmitter.addListener('SplitViewChanged', this.onSplitViewChanged);
|
||||
Linking.addEventListener('url', this.onDeepLink);
|
||||
}
|
||||
|
||||
@@ -93,6 +103,38 @@ class GlobalEventHandler {
|
||||
}
|
||||
};
|
||||
|
||||
onSplitViewChanged = async (result: SplitViewResult) => {
|
||||
if (result.isTablet != null && Device.IS_TABLET !== result.isTablet) {
|
||||
Device.IS_TABLET = result.isTablet;
|
||||
const serverUrl = await getActiveServerUrl();
|
||||
if (serverUrl && result.isTablet) {
|
||||
try {
|
||||
const {database} = DatabaseManager.getServerDatabaseAndOperator(serverUrl);
|
||||
const {currentChannelId, currentTeamId} = await getCommonSystemValues(database);
|
||||
if (currentTeamId && !currentChannelId) {
|
||||
let channelId = '';
|
||||
const teamChannelHistory = await getTeamChannelHistory(database, currentTeamId);
|
||||
if (teamChannelHistory.length) {
|
||||
channelId = teamChannelHistory[0];
|
||||
} else {
|
||||
const defaultChannel = await queryTeamDefaultChannel(database, currentTeamId).fetch();
|
||||
if (defaultChannel.length) {
|
||||
channelId = defaultChannel[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
switchToChannelById(serverUrl, channelId);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// do nothing, the UI will not show a channel but that is fixed when the user picks one.
|
||||
}
|
||||
}
|
||||
setScreensOrientation(result.isTablet);
|
||||
}
|
||||
};
|
||||
|
||||
serverUpgradeNeeded = async (serverUrl: string) => {
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import CompassIcon from '@components/compass_icon';
|
||||
import {Screens} from '@constants';
|
||||
import {CURRENT_CALL_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import {allOrientations, dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {displayUsername} from '@utils/user';
|
||||
|
||||
@@ -105,7 +105,7 @@ const CurrentCallBar = ({
|
||||
layout: {
|
||||
backgroundColor: '#000',
|
||||
componentBackgroundColor: '#000',
|
||||
orientation: ['portrait', 'landscape'],
|
||||
orientation: allOrientations,
|
||||
},
|
||||
topBar: {
|
||||
background: {
|
||||
|
||||
@@ -40,12 +40,14 @@ import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {
|
||||
bottomSheet,
|
||||
dismissAllModalsAndPopToScreen,
|
||||
dismissBottomSheet,
|
||||
goToScreen,
|
||||
popTopScreen,
|
||||
setScreensOrientation,
|
||||
} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {bottomSheetSnapPoint} from '@utils/helpers';
|
||||
@@ -261,6 +263,7 @@ const CallScreen = ({
|
||||
const theme = useTheme();
|
||||
const {bottom} = useSafeAreaInsets();
|
||||
const {width, height} = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
const serverUrl = useServerUrl();
|
||||
const {EnableRecordings} = useCallsConfig(serverUrl);
|
||||
usePermissionsChecker(micPermissionsGranted);
|
||||
@@ -432,6 +435,16 @@ const CallScreen = ({
|
||||
return () => listener.remove();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const didDismissListener = Navigation.events().registerComponentDidDisappearListener(async ({componentId: screen}) => {
|
||||
if (componentId === screen) {
|
||||
setScreensOrientation(isTablet);
|
||||
}
|
||||
});
|
||||
|
||||
return () => didDismissListener.remove();
|
||||
}, [isTablet]);
|
||||
|
||||
if (!currentCall || !myParticipant) {
|
||||
// Note: this happens because the screen is "rendered", even after the screen has been popped, and the
|
||||
// currentCall will have already been set to null when those extra renders run. We probably don't ever need
|
||||
|
||||
@@ -187,7 +187,7 @@ export const queryAllMyChannel = (database: Database) => {
|
||||
};
|
||||
|
||||
export const queryAllMyChannelsForTeam = (database: Database, teamId: string) => {
|
||||
return database.get<ChannelModel>(MY_CHANNEL).query(
|
||||
return database.get<MyChannelModel>(MY_CHANNEL).query(
|
||||
Q.on(CHANNEL, Q.where('team_id', Q.oneOf([teamId, '']))),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
|
||||
import React, {useCallback, useMemo, useRef, useState} from 'react';
|
||||
import {NativeModules, useWindowDimensions, Platform} from 'react-native';
|
||||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {useGalleryControls} from '@hooks/gallery';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import {dismissOverlay, setScreensOrientation} from '@screens/navigation';
|
||||
import {freezeOtherScreens} from '@utils/gallery';
|
||||
|
||||
import Footer from './footer';
|
||||
@@ -46,14 +45,10 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde
|
||||
}, []);
|
||||
|
||||
const close = useCallback(() => {
|
||||
setScreensOrientation(isTablet);
|
||||
if (Platform.OS === 'ios' && !isTablet) {
|
||||
// We need both the navigation & the module
|
||||
Navigation.setDefaultOptions({
|
||||
layout: {
|
||||
orientation: ['portrait'],
|
||||
},
|
||||
});
|
||||
NativeModules.MattermostManaged.lockPortrait();
|
||||
NativeModules.SplitView.lockPortrait();
|
||||
}
|
||||
freezeOtherScreens(false);
|
||||
requestAnimationFrame(async () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||
animatedStyle={
|
||||
{
|
||||
"value": {
|
||||
"maxWidth": "100%",
|
||||
"maxWidth": 750,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ exports[`components/categories_list should render channels error 1`] = `
|
||||
{
|
||||
"backgroundColor": "#1e325c",
|
||||
"flex": 1,
|
||||
"maxWidth": "100%",
|
||||
"maxWidth": 750,
|
||||
"paddingLeft": 18,
|
||||
"paddingRight": 20,
|
||||
"paddingTop": 10,
|
||||
@@ -340,7 +340,7 @@ exports[`components/categories_list should render team error 1`] = `
|
||||
animatedStyle={
|
||||
{
|
||||
"value": {
|
||||
"maxWidth": "100%",
|
||||
"maxWidth": 750,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -349,7 +349,7 @@ exports[`components/categories_list should render team error 1`] = `
|
||||
{
|
||||
"backgroundColor": "#1e325c",
|
||||
"flex": 1,
|
||||
"maxWidth": "100%",
|
||||
"maxWidth": 750,
|
||||
"paddingLeft": 18,
|
||||
"paddingRight": 20,
|
||||
"paddingTop": 10,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {act} from 'react-test-renderer';
|
||||
|
||||
import {renderWithEverything} from '@test/intl-test-helper';
|
||||
import TestHelper from '@test/test_helper';
|
||||
@@ -19,15 +18,11 @@ describe('components/channel_list/categories', () => {
|
||||
});
|
||||
|
||||
it('render without error', () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = renderWithEverything(
|
||||
<Categories/>,
|
||||
{database},
|
||||
);
|
||||
|
||||
act(() => jest.runAllTimers());
|
||||
|
||||
expect(wrapper.toJSON()).toBeTruthy();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,7 +46,19 @@ const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => {
|
||||
const isTablet = useIsTablet();
|
||||
const switchingTeam = useTeamSwitch();
|
||||
const teamId = categories[0]?.teamId;
|
||||
const [initiaLoad, setInitialLoad] = useState(true);
|
||||
const categoriesToShow = useMemo(() => {
|
||||
if (onlyUnreads && !unreadsOnTop) {
|
||||
return ['UNREADS' as const];
|
||||
}
|
||||
const orderedCategories = [...categories];
|
||||
orderedCategories.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
if (unreadsOnTop) {
|
||||
return ['UNREADS' as const, ...orderedCategories];
|
||||
}
|
||||
return orderedCategories;
|
||||
}, [categories, onlyUnreads, unreadsOnTop]);
|
||||
|
||||
const [initiaLoad, setInitialLoad] = useState(!categoriesToShow.length);
|
||||
|
||||
const onChannelSwitch = useCallback(async (channelId: string) => {
|
||||
switchToChannelById(serverUrl, channelId);
|
||||
@@ -76,18 +88,6 @@ const Categories = ({categories, onlyUnreads, unreadsOnTop}: Props) => {
|
||||
);
|
||||
}, [teamId, intl.locale, isTablet, onChannelSwitch, onlyUnreads]);
|
||||
|
||||
const categoriesToShow = useMemo(() => {
|
||||
if (onlyUnreads && !unreadsOnTop) {
|
||||
return ['UNREADS' as const];
|
||||
}
|
||||
const orderedCategories = [...categories];
|
||||
orderedCategories.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||
if (unreadsOnTop) {
|
||||
return ['UNREADS' as const, ...orderedCategories];
|
||||
}
|
||||
return orderedCategories;
|
||||
}, [categories, onlyUnreads, unreadsOnTop]);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setTimeout(() => {
|
||||
setInitialLoad(false);
|
||||
|
||||
@@ -33,7 +33,6 @@ describe('components/categories_list', () => {
|
||||
it('should render', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
isTablet={false}
|
||||
teamsCount={1}
|
||||
channelsCount={1}
|
||||
/>,
|
||||
@@ -47,7 +46,6 @@ describe('components/categories_list', () => {
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
isCRTEnabled={true}
|
||||
isTablet={false}
|
||||
teamsCount={1}
|
||||
channelsCount={1}
|
||||
/>,
|
||||
@@ -69,7 +67,6 @@ describe('components/categories_list', () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
isTablet={false}
|
||||
teamsCount={0}
|
||||
channelsCount={1}
|
||||
/>,
|
||||
@@ -92,7 +89,6 @@ describe('components/categories_list', () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = renderWithEverything(
|
||||
<CategoriesList
|
||||
isTablet={false}
|
||||
teamsCount={1}
|
||||
channelsCount={0}
|
||||
/>,
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useEffect} from 'react';
|
||||
import {useWindowDimensions} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import ThreadsButton from '@components/threads_button';
|
||||
import {TABLET_SIDEBAR_WIDTH, TEAM_SIDEBAR_WIDTH} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import Categories from './categories';
|
||||
@@ -28,7 +30,6 @@ type ChannelListProps = {
|
||||
channelsCount: number;
|
||||
iconPad?: boolean;
|
||||
isCRTEnabled?: boolean;
|
||||
isTablet: boolean;
|
||||
teamsCount: number;
|
||||
};
|
||||
|
||||
@@ -36,9 +37,11 @@ const getTabletWidth = (teamsCount: number) => {
|
||||
return TABLET_SIDEBAR_WIDTH - (teamsCount > 1 ? TEAM_SIDEBAR_WIDTH : 0);
|
||||
};
|
||||
|
||||
const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, isTablet, teamsCount}: ChannelListProps) => {
|
||||
const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, teamsCount}: ChannelListProps) => {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
const {width} = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
const tabletWidth = useSharedValue(isTablet ? getTabletWidth(teamsCount) : 0);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -50,12 +53,12 @@ const CategoriesList = ({channelsCount, iconPad, isCRTEnabled, isTablet, teamsCo
|
||||
const tabletStyle = useAnimatedStyle(() => {
|
||||
if (!isTablet) {
|
||||
return {
|
||||
maxWidth: '100%',
|
||||
maxWidth: width,
|
||||
};
|
||||
}
|
||||
|
||||
return {maxWidth: withTiming(tabletWidth.value, {duration: 350})};
|
||||
}, [isTablet]);
|
||||
}, [isTablet, width]);
|
||||
|
||||
let content;
|
||||
|
||||
|
||||
@@ -183,7 +183,6 @@ const ChannelListScreen = (props: ChannelProps) => {
|
||||
<CategoriesList
|
||||
iconPad={canAddOtherServers && props.teamsCount <= 1}
|
||||
isCRTEnabled={props.isCRTEnabled}
|
||||
isTablet={isTablet}
|
||||
teamsCount={props.teamsCount}
|
||||
channelsCount={props.channelsCount}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import merge from 'deepmerge';
|
||||
import {Appearance, DeviceEventEmitter, NativeModules, StatusBar, Platform, Alert} from 'react-native';
|
||||
import {ComponentWillAppearEvent, ImageResource, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation';
|
||||
import {ComponentWillAppearEvent, ImageResource, LayoutOrientation, Navigation, Options, OptionsModalPresentationStyle, OptionsTopBarButton, ScreenPoppedEvent} from 'react-native-navigation';
|
||||
import tinyColor from 'tinycolor2';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
@@ -21,8 +21,8 @@ import type {BottomSheetFooterProps} from '@gorhom/bottom-sheet';
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
import type {AvailableScreens, NavButtons} from '@typings/screens/navigation';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
const {SplitView} = NativeModules;
|
||||
const {isRunningInSplitView} = SplitView;
|
||||
|
||||
const alpha = {
|
||||
from: 0,
|
||||
@@ -30,6 +30,9 @@ const alpha = {
|
||||
duration: 150,
|
||||
};
|
||||
|
||||
export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
|
||||
export const portraitOrientation: LayoutOrientation[] = ['portrait'];
|
||||
|
||||
export function registerNavigationListeners() {
|
||||
Navigation.events().registerScreenPoppedListener(onPoppedListener);
|
||||
Navigation.events().registerCommandListener(onCommandListener);
|
||||
@@ -185,7 +188,7 @@ Navigation.setDefaultOptions({
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
orientation: Device.IS_TABLET ? undefined : ['portrait'],
|
||||
orientation: Device.IS_TABLET ? allOrientations : portraitOrientation,
|
||||
},
|
||||
topBar: {
|
||||
title: {
|
||||
@@ -218,6 +221,19 @@ Appearance.addChangeListener(() => {
|
||||
}
|
||||
});
|
||||
|
||||
export function setScreensOrientation(allowRotation: boolean) {
|
||||
const options: Options = {
|
||||
layout: {
|
||||
orientation: allowRotation ? allOrientations : portraitOrientation,
|
||||
},
|
||||
};
|
||||
Navigation.setDefaultOptions(options);
|
||||
const screens = NavigationStore.getScreensInStack();
|
||||
for (const s of screens) {
|
||||
Navigation.mergeOptions(s, options);
|
||||
}
|
||||
}
|
||||
|
||||
export function getThemeFromState(): Theme {
|
||||
return EphemeralStore.theme || getDefaultThemeByAppearance();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {Navigation, Options, OptionsLayout} from 'react-native-navigation';
|
||||
import {measure} from 'react-native-reanimated';
|
||||
|
||||
import {Events, Screens} from '@constants';
|
||||
import {showOverlay} from '@screens/navigation';
|
||||
import {allOrientations, showOverlay} from '@screens/navigation';
|
||||
import {isImage, isVideo} from '@utils/file';
|
||||
import {generateId} from '@utils/general';
|
||||
|
||||
@@ -127,7 +127,7 @@ export function openGalleryAtIndex(galleryIdentifier: string, initialIndex: numb
|
||||
items,
|
||||
};
|
||||
const layout: OptionsLayout = {
|
||||
orientation: ['portrait', 'landscape'],
|
||||
orientation: allOrientations,
|
||||
};
|
||||
const options: Options = {
|
||||
layout,
|
||||
@@ -155,7 +155,7 @@ export function openGalleryAtIndex(galleryIdentifier: string, initialIndex: numb
|
||||
if (Platform.OS === 'ios') {
|
||||
// on iOS we need both the navigation & the module
|
||||
Navigation.setDefaultOptions({layout});
|
||||
NativeModules.MattermostManaged.unlockOrientation();
|
||||
NativeModules.SplitView.unlockOrientation();
|
||||
}
|
||||
showOverlay(Screens.GALLERY, props, options);
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ import {Device} from '@constants';
|
||||
import {CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES} from '@constants/custom_status';
|
||||
import {STATUS_BAR_HEIGHT} from '@constants/view';
|
||||
|
||||
const {MattermostManaged} = NativeModules;
|
||||
const isRunningInSplitView = MattermostManaged.isRunningInSplitView;
|
||||
const {SplitView} = NativeModules;
|
||||
const {isRunningInSplitView} = SplitView;
|
||||
const ShareModule: NativeShareExtension|undefined = Platform.select({android: NativeModules.MattermostShare});
|
||||
|
||||
// isMinimumServerVersion will return true if currentVersion is equal to higher or than
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
7F537B2928A517580086D6B3 /* NotificationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F537B2828A517580086D6B3 /* NotificationHelper.swift */; };
|
||||
7F581D35221ED5C60099E66B /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F581D34221ED5C60099E66B /* NotificationService.swift */; };
|
||||
7F581D39221ED5C60099E66B /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 7F581D32221ED5C60099E66B /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
7F59882B29827F2200C8C108 /* SplitViewModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F59882A29827F2200C8C108 /* SplitViewModule.swift */; };
|
||||
7F59882E2982850000C8C108 /* SplitViewModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F59882D2982850000C8C108 /* SplitViewModule.m */; };
|
||||
7F7E9F462864E6C60064BFAF /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E9F442864E6C60064BFAF /* Color.swift */; };
|
||||
7F7E9F472864E6C60064BFAF /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E9F452864E6C60064BFAF /* View.swift */; };
|
||||
7F7E9F4C2864E6EC0064BFAF /* AttachmentModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F7E9F492864E6EB0064BFAF /* AttachmentModel.swift */; };
|
||||
@@ -196,6 +198,8 @@
|
||||
7F581D34221ED5C60099E66B /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
7F581D36221ED5C60099E66B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
7F581F77221EEA5A0099E66B /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = "<group>"; };
|
||||
7F59882A29827F2200C8C108 /* SplitViewModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SplitViewModule.swift; path = Mattermost/Modules/SplitViewModule.swift; sourceTree = "<group>"; };
|
||||
7F59882D2982850000C8C108 /* SplitViewModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SplitViewModule.m; path = Mattermost/Modules/SplitViewModule.m; sourceTree = "<group>"; };
|
||||
7F63D2C21E6DD98A001FAE12 /* Mattermost.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Mattermost.entitlements; path = Mattermost/Mattermost.entitlements; sourceTree = "<group>"; };
|
||||
7F7E9F442864E6C60064BFAF /* Color.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = "<group>"; };
|
||||
7F7E9F452864E6C60064BFAF /* View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
|
||||
@@ -465,6 +469,8 @@
|
||||
7FEB109A1F61019C0039A015 /* MattermostManaged.m */,
|
||||
7FEC870428A44A7B00DE96CB /* NotificationsModule.h */,
|
||||
7FEC870028A4325D00DE96CB /* NotificationsModule.m */,
|
||||
7F59882A29827F2200C8C108 /* SplitViewModule.swift */,
|
||||
7F59882D2982850000C8C108 /* SplitViewModule.m */,
|
||||
);
|
||||
name = Modules;
|
||||
sourceTree = "<group>";
|
||||
@@ -800,8 +806,8 @@
|
||||
);
|
||||
mainGroup = 83CBB9F61A601CBA00E9B192;
|
||||
packageReferences = (
|
||||
7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */,
|
||||
27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa.git" */,
|
||||
7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */,
|
||||
27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */,
|
||||
);
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -1008,12 +1014,14 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7FEC870128A4325D00DE96CB /* NotificationsModule.m in Sources */,
|
||||
7F59882E2982850000C8C108 /* SplitViewModule.m in Sources */,
|
||||
7F1EB88527FDE361002E7EEC /* GekidouWrapper.swift in Sources */,
|
||||
7FCEFB9326B7934F006DC1DE /* SDWebImageDownloaderOperation+Swizzle.m in Sources */,
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
7F151D3E221B062700FAD8F3 /* RuntimeUtils.swift in Sources */,
|
||||
7F537B2928A517580086D6B3 /* NotificationHelper.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
7F59882B29827F2200C8C108 /* SplitViewModule.swift in Sources */,
|
||||
7FEB109D1F61019C0039A015 /* MattermostManaged.m in Sources */,
|
||||
536CC6C323E79287002C478C /* RNNotificationEventHandler+HandleReplyAction.m in Sources */,
|
||||
);
|
||||
@@ -1523,7 +1531,7 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa.git" */ = {
|
||||
27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/getsentry/sentry-cocoa.git";
|
||||
requirement = {
|
||||
@@ -1531,7 +1539,7 @@
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */ = {
|
||||
7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/satoshi-takano/OpenGraph.git";
|
||||
requirement = {
|
||||
@@ -1544,12 +1552,12 @@
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
27C667A229523ECA00E590D5 /* Sentry */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa.git" */;
|
||||
package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
|
||||
productName = Sentry;
|
||||
};
|
||||
27C667A429523F0A00E590D5 /* Sentry */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa.git" */;
|
||||
package = 27C667A129523ECA00E590D5 /* XCRemoteSwiftPackageReference "sentry-cocoa" */;
|
||||
productName = Sentry;
|
||||
};
|
||||
49AE370026D4455D00EF4E52 /* Gekidou */ = {
|
||||
@@ -1566,7 +1574,7 @@
|
||||
};
|
||||
7FD4822B2864D73300A5B18B /* OpenGraph */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph.git" */;
|
||||
package = 7FD4822A2864D73300A5B18B /* XCRemoteSwiftPackageReference "OpenGraph" */;
|
||||
productName = OpenGraph;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
#import <React/RCTConstants.h>
|
||||
#import "AppDelegate.h"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// See License.txt for license information.
|
||||
//
|
||||
|
||||
#import "AppDelegate.h"
|
||||
#import "MattermostManaged.h"
|
||||
#import "CreateThumbnail.h"
|
||||
#import "Mattermost-Swift.h"
|
||||
@@ -52,21 +51,6 @@ RCT_EXPORT_MODULE();
|
||||
};
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(isRunningInSplitView:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
|
||||
dispatch_block_t splitViewCheck = ^{
|
||||
BOOL isRunningInFullScreen = CGRectEqualToRect(
|
||||
[UIApplication sharedApplication].delegate.window.frame,
|
||||
[UIApplication sharedApplication].delegate.window.screen.bounds);
|
||||
resolve(@{
|
||||
@"isSplitView": @(!isRunningInFullScreen)
|
||||
});
|
||||
};
|
||||
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {
|
||||
splitViewCheck();
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), splitViewCheck);
|
||||
}
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(deleteDatabaseDirectory: (NSString *)databaseName shouldRemoveDirectory: (BOOL) shouldRemoveDirectory callback: (RCTResponseSenderBlock)callback){
|
||||
@try {
|
||||
@@ -169,37 +153,6 @@ RCT_EXPORT_METHOD(removeListeners:(double)count) {
|
||||
// Keep: Required for RN built in Event Emitter Calls.
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(unlockOrientation)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
AppDelegate * appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
|
||||
appDelegate.allowRotation = YES;
|
||||
NSNumber *resetOrientationTarget = [NSNumber numberWithInt:UIInterfaceOrientationUnknown];
|
||||
|
||||
[[UIDevice currentDevice] setValue:resetOrientationTarget forKey:@"orientation"];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(lockPortrait)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
AppDelegate * appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
|
||||
|
||||
appDelegate.allowRotation = NO;
|
||||
NSNumber *resetOrientationTarget = [NSNumber numberWithInt:UIInterfaceOrientationUnknown];
|
||||
|
||||
[[UIDevice currentDevice] setValue:resetOrientationTarget forKey:@"orientation"];
|
||||
|
||||
|
||||
|
||||
NSNumber *orientationTarget = [NSNumber numberWithInt:UIInterfaceOrientationPortrait];
|
||||
|
||||
[[UIDevice currentDevice] setValue:orientationTarget forKey:@"orientation"];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(createThumbnail:(NSDictionary *)config findEventsWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
|
||||
{
|
||||
|
||||
13
ios/Mattermost/Modules/SplitViewModule.m
Normal file
13
ios/Mattermost/Modules/SplitViewModule.m
Normal file
@@ -0,0 +1,13 @@
|
||||
#import <React/RCTBridgeModule.h>
|
||||
#import <React/RCTEventEmitter.h>
|
||||
|
||||
@interface RCT_EXTERN_REMAP_MODULE(SplitView, SplitViewModule, RCTEventEmitter)
|
||||
RCT_EXTERN_METHOD(supportedEvents)
|
||||
RCT_EXTERN_METHOD(startObserving)
|
||||
RCT_EXTERN_METHOD(stopObserving)
|
||||
RCT_EXTERN_METHOD(isRunningInSplitView:
|
||||
(RCTPromiseResolveBlock)resolve
|
||||
withRejecter:(RCTPromiseRejectBlock)reject)
|
||||
RCT_EXTERN_METHOD(unlockOrientation)
|
||||
RCT_EXTERN_METHOD(lockPortrait)
|
||||
@end
|
||||
75
ios/Mattermost/Modules/SplitViewModule.swift
Normal file
75
ios/Mattermost/Modules/SplitViewModule.swift
Normal file
@@ -0,0 +1,75 @@
|
||||
import Foundation
|
||||
|
||||
@objc(SplitViewModule)
|
||||
class SplitViewModule: RCTEventEmitter {
|
||||
var hasListeners = false
|
||||
|
||||
@objc
|
||||
override static func requiresMainQueueSetup() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@objc
|
||||
override func supportedEvents() -> [String]! {
|
||||
return ["SplitViewChanged"]
|
||||
}
|
||||
|
||||
@objc
|
||||
override func startObserving() {
|
||||
hasListeners = true
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(isSplitView), name: NSNotification.Name.RCTUserInterfaceStyleDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
override func stopObserving() {
|
||||
hasListeners = false
|
||||
NotificationCenter.default.removeObserver(self,
|
||||
name: NSNotification.Name.RCTUserInterfaceStyleDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc func isRunningInFullScreen() -> Bool {
|
||||
guard let w = UIApplication.shared.delegate?.window, let window = w else { return false }
|
||||
return window.frame.equalTo(window.screen.bounds)
|
||||
}
|
||||
|
||||
@objc func isSplitView() {
|
||||
if hasListeners && UIDevice.current.userInterfaceIdiom == .pad {
|
||||
sendEvent(withName: "SplitViewChanged", body: [
|
||||
"isSplitView": !isRunningInFullScreen(),
|
||||
"isTablet": UIDevice.current.userInterfaceIdiom == .pad,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@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(unlockOrientation)
|
||||
func unlockOrientation() {
|
||||
DispatchQueue.main.async {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.allowRotation = true
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.unknown, forKey: "orientation")
|
||||
}
|
||||
}
|
||||
|
||||
@objc(lockPortrait)
|
||||
func lockPortrait() {
|
||||
DispatchQueue.main.async {
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.allowRotation = false
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.unknown, forKey: "orientation")
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait, forKey: "orientation")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -334,7 +334,7 @@ PODS:
|
||||
- glog
|
||||
- react-native-background-timer (2.4.1):
|
||||
- React-Core
|
||||
- react-native-cameraroll (5.2.2):
|
||||
- react-native-cameraroll (5.2.3):
|
||||
- React-Core
|
||||
- react-native-cookies (6.2.1):
|
||||
- React-Core
|
||||
@@ -535,7 +535,7 @@ PODS:
|
||||
- RNScreens (3.19.0):
|
||||
- React-Core
|
||||
- React-RCTImage
|
||||
- RNSentry (4.13.0):
|
||||
- RNSentry (4.14.0):
|
||||
- React-Core
|
||||
- Sentry/HybridSDK (= 7.31.5)
|
||||
- RNShare (8.1.0):
|
||||
@@ -929,7 +929,7 @@ SPEC CHECKSUMS:
|
||||
React-jsinspector: ff56004b0c974b688a6548c156d5830ad751ae07
|
||||
React-logger: 60a0b5f8bed667ecf9e24fecca1f30d125de6d75
|
||||
react-native-background-timer: 17ea5e06803401a379ebf1f20505b793ac44d0fe
|
||||
react-native-cameraroll: 71d68167beb6fc7216aa564abb6d86f1d666a2c6
|
||||
react-native-cameraroll: 5b25d0be40185d02e522bf2abf8a1ba4e8faa107
|
||||
react-native-cookies: f54fcded06bb0cda05c11d86788020b43528a26c
|
||||
react-native-create-thumbnail: e022bcdcba8a0b4529a50d3fa1a832ec921be39d
|
||||
react-native-document-picker: 958e2bc82e128be69055be261aeac8d872c8d34c
|
||||
@@ -977,7 +977,7 @@ SPEC CHECKSUMS:
|
||||
RNReanimated: cc5e3aa479cb9170bcccf8204291a6950a3be128
|
||||
RNRudderSdk: 07f732edfe473ef67b8cc6ad1800ddb81b78286f
|
||||
RNScreens: ea4cd3a853063cda19a4e3c28d2e52180c80f4eb
|
||||
RNSentry: acebe4104a6f5915ae871eb59dc73f13dcc92ef7
|
||||
RNSentry: 7e90aec2633d2fdad8aeb839c9915e4376fd27d1
|
||||
RNShare: 48b3113cd089a2be8ff0515c3ae7a46a4db8a76b
|
||||
RNSVG: d787d64ca06b9158e763ad2638a8c4edce00782a
|
||||
RNVectorIcons: fcc2f6cb32f5735b586e66d14103a74ce6ad61f8
|
||||
|
||||
1533
package-lock.json
generated
1533
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -23,7 +23,7 @@
|
||||
"@msgpack/msgpack": "2.8.0",
|
||||
"@nozbe/watermelondb": "0.25.1",
|
||||
"@nozbe/with-observables": "1.4.1",
|
||||
"@react-native-camera-roll/camera-roll": "5.2.2",
|
||||
"@react-native-camera-roll/camera-roll": "5.2.3",
|
||||
"@react-native-clipboard/clipboard": "1.11.1",
|
||||
"@react-native-community/datetimepicker": "6.7.3",
|
||||
"@react-native-community/netinfo": "9.3.7",
|
||||
@@ -32,7 +32,7 @@
|
||||
"@react-navigation/native": "6.1.2",
|
||||
"@react-navigation/stack": "6.3.11",
|
||||
"@rudderstack/rudder-sdk-react-native": "1.5.2",
|
||||
"@sentry/react-native": "4.13.0",
|
||||
"@sentry/react-native": "4.14.0",
|
||||
"@stream-io/flat-list-mvcp": "0.10.2",
|
||||
"base-64": "1.0.0",
|
||||
"commonmark": "npm:@mattermost/commonmark@0.30.1-0",
|
||||
@@ -118,7 +118,7 @@
|
||||
"@types/commonmark": "0.27.5",
|
||||
"@types/commonmark-react-renderer": "4.3.1",
|
||||
"@types/deep-equal": "1.0.1",
|
||||
"@types/jest": "29.2.6",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/lodash": "4.14.191",
|
||||
"@types/mime-db": "1.43.1",
|
||||
"@types/querystringify": "2.0.0",
|
||||
@@ -139,9 +139,9 @@
|
||||
"@types/uuid": "9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.49.0",
|
||||
"@typescript-eslint/parser": "5.49.0",
|
||||
"axios": "1.2.3",
|
||||
"axios": "1.2.4",
|
||||
"axios-cookiejar-support": "4.0.6",
|
||||
"babel-jest": "29.3.1",
|
||||
"babel-jest": "29.4.0",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-module-resolver": "5.0.0",
|
||||
"deep-freeze": "0.0.1",
|
||||
@@ -154,8 +154,8 @@
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"husky": "8.0.3",
|
||||
"isomorphic-fetch": "3.0.0",
|
||||
"jest": "29.3.1",
|
||||
"jest-cli": "29.3.1",
|
||||
"jest": "29.4.0",
|
||||
"jest-cli": "29.4.0",
|
||||
"jetifier": "2.0.0",
|
||||
"metro-react-native-babel-preset": "0.74.1",
|
||||
"mmjstool": "github:mattermost/mattermost-utilities#010f456ea8be5beebafdb8776177cba515c1969e",
|
||||
|
||||
@@ -92,6 +92,11 @@ jest.doMock('react-native', () => {
|
||||
},
|
||||
}),
|
||||
},
|
||||
SplitView: {
|
||||
addListener: jest.fn(),
|
||||
removeListeners: jest.fn(),
|
||||
isRunningInSplitView: jest.fn().mockResolvedValue(() => ({isSplitView: false, isTablet: false})),
|
||||
},
|
||||
Notifications: {
|
||||
getDeliveredNotifications: jest.fn().mockResolvedValue([]),
|
||||
removeChannelNotifications: jest.fn().mockImplementation(),
|
||||
|
||||
7
types/global/device.d.ts
vendored
Normal file
7
types/global/device.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
type SplitViewResult = {
|
||||
isSplitView?: boolean;
|
||||
isTablet?: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user