diff --git a/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx b/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx
index db6f07a3f..81f123ba5 100644
--- a/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx
+++ b/app/components/channel_bookmarks/channel_bookmark/channel_bookmark.tsx
@@ -6,6 +6,7 @@ import {Button} from '@rneui/base';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import {useIntl, type IntlShape} from 'react-intl';
import {Alert, StyleSheet} from 'react-native';
+import Animated from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {ITEM_HEIGHT} from '@components/option_item';
@@ -149,20 +150,19 @@ const ChannelBookmark = ({
}
return (
-
+
+
+
);
};
diff --git a/app/hooks/device.ts b/app/hooks/device.ts
index f9d14cf03..410437b01 100644
--- a/app/hooks/device.ts
+++ b/app/hooks/device.ts
@@ -1,14 +1,17 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
+import RNUtils, {type WindowDimensions} from '@mattermost/rnutils';
import React, {type RefObject, useEffect, useRef, useState, useContext} from 'react';
-import {AppState, DeviceEventEmitter, Keyboard, Platform, useWindowDimensions, View} from 'react-native';
+import {AppState, DeviceEventEmitter, Keyboard, NativeEventEmitter, Platform, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {DeviceContext} from '@context/device';
import type {KeyboardTrackingViewRef, KeyboardWillShowEventData} from '@mattermost/keyboard-tracker';
+const utilsEmitter = new NativeEventEmitter(RNUtils);
+
export function useSplitView() {
const {isSplit} = React.useContext(DeviceContext);
return isSplit;
@@ -28,6 +31,20 @@ export function useAppState() {
return appState;
}
+export function useWindowDimensions() {
+ const [dimensions, setDimensions] = useState(RNUtils.getWindowDimensions());
+
+ useEffect(() => {
+ const listener = utilsEmitter.addListener('DimensionsChanged', (window: WindowDimensions) => {
+ setDimensions(window);
+ });
+
+ return () => listener.remove();
+ }, []);
+
+ return dimensions;
+}
+
export function useIsTablet() {
const {isSplit, isTablet} = useContext(DeviceContext);
return isTablet && !isSplit;
diff --git a/app/screens/gallery/index.tsx b/app/screens/gallery/index.tsx
index 8188af25d..e3a9015fa 100644
--- a/app/screens/gallery/index.tsx
+++ b/app/screens/gallery/index.tsx
@@ -3,12 +3,12 @@
import RNUtils from '@mattermost/rnutils';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {useWindowDimensions, Platform} from 'react-native';
+import {Platform} from 'react-native';
import {CaptionsEnabledContext} from '@calls/context';
import {hasCaptions} from '@calls/utils';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
-import {useIsTablet} from '@hooks/device';
+import {useIsTablet, useWindowDimensions} from '@hooks/device';
import {useGalleryControls} from '@hooks/gallery';
import {dismissOverlay, setScreensOrientation} from '@screens/navigation';
import {freezeOtherScreens} from '@utils/gallery';
@@ -35,7 +35,7 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde
const [captionsEnabled, setCaptionsEnabled] = useState(new Array(items.length).fill(true));
const [captionsAvailable, setCaptionsAvailable] = useState([]);
const {setControlsHidden, headerStyles, footerStyles} = useGalleryControls();
- const dimensions = useMemo(() => ({width: dim.width, height: dim.height}), [dim.width]);
+ const dimensions = useMemo(() => ({width: dim.width, height: dim.height}), [dim]);
const galleryRef = useRef(null);
useEffect(() => {
diff --git a/app/screens/gallery/video_renderer/index.tsx b/app/screens/gallery/video_renderer/index.tsx
index f539eab9f..fa96ba378 100644
--- a/app/screens/gallery/video_renderer/index.tsx
+++ b/app/screens/gallery/video_renderer/index.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
-import {DeviceEventEmitter, StyleSheet, useWindowDimensions} from 'react-native';
+import {DeviceEventEmitter, StyleSheet} from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
@@ -47,7 +47,6 @@ const styles = StyleSheet.create({
});
const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShouldHideControls, width}: VideoRendererProps) => {
- const dimensions = useWindowDimensions();
const fullscreen = useSharedValue(false);
const {bottom} = useSafeAreaInsets();
const serverUrl = useServerUrl();
@@ -126,23 +125,15 @@ const VideoRenderer = ({height, index, initialIndex, item, isPageActive, onShoul
}, []);
const dimensionsStyle = useMemo(() => {
- let w = width;
- let h = height - (VIDEO_INSET + GALLERY_FOOTER_HEIGHT + bottom);
+ const w = width;
+ const h = height - (VIDEO_INSET + GALLERY_FOOTER_HEIGHT + bottom);
if (hasError) {
return {height: 0, width: 0};
}
- if (fullscreen.value) {
- w = dimensions.width;
- h = dimensions.height;
- } else if (dimensions.width > dimensions.height) {
- w = h;
- h = width;
- }
-
return {width: w, height: h};
- }, [hasError, fullscreen.value, dimensions.height]);
+ }, [hasError, fullscreen.value, height, width]);
const animatedStyle = useAnimatedStyle(() => {
return {
diff --git a/app/screens/gallery/viewer/index.tsx b/app/screens/gallery/viewer/index.tsx
index 730648206..35cca69a7 100644
--- a/app/screens/gallery/viewer/index.tsx
+++ b/app/screens/gallery/viewer/index.tsx
@@ -120,7 +120,7 @@ const GalleryViewer = ({
}
return ();
- }, [items]);
+ }, [items, width, height]);
return (
(CHANNEL_PATH);
type PermalinkPathParams = {
- serverUrl: string;
+ serverUrl: string[];
teamName: string;
postId: string;
};
-const PERMALINK_PATH = `:serverUrl(.*)/:teamName(${TEAM_NAME_PATH_PATTERN})/pl/:postId(${ID_PATH_PATTERN})`;
+const PERMALINK_PATH = '*serverUrl/:teamName/pl/:postId';
export const matchPermalinkDeeplink = match(PERMALINK_PATH);
type ServerPathParams = {
serverUrl: string;
path: string;
+ subpath?: string[];
}
-export const matchServerDeepLink = match(':serverUrl(.*)/:path(.*)', {decode: decodeURIComponent});
+export const matchServerDeepLink = match(':serverUrl/{:path}{/*subpath}', {decode: decodeURIComponent});
const reservedWords = ['login', 'signup', 'admin_console'];
export function extractServerUrl(url: string) {
@@ -168,43 +168,72 @@ export function extractServerUrl(url: string) {
const matched = matchServerDeepLink(deepLinkUrl);
if (matched) {
- const {path} = matched.params;
- const segments = path.split('/');
+ const {path, subpath} = matched.params;
- if (segments.length > 0 && reservedWords.includes(segments[segments.length - 1])) {
- return matched.params.serverUrl;
+ let extra = '';
+
+ if (!path || reservedWords.includes(path)) {
+ return stripTrailingSlashes(matched.params.serverUrl);
}
- return path ? `${matched.params.serverUrl}/${path}` : matched.params.serverUrl;
+
+ if (subpath && subpath.length > 0) {
+ if (reservedWords.includes(subpath[subpath.length - 1])) {
+ subpath.pop();
+ }
+
+ extra = subpath.join('/');
+ }
+
+ if (extra) {
+ return stripTrailingSlashes(`${matched.params.serverUrl}/${path}/${extra}`);
+ }
+
+ return stripTrailingSlashes(`${matched.params.serverUrl}/${path}`);
}
return deepLinkUrl;
}
+function isValidTeamName(teamName: string): boolean {
+ const regex = new RegExp(`^${TEAM_NAME_PATH_PATTERN}$`);
+ return regex.test(teamName);
+}
+
+function isValidIdentifierPathPattern(id: string): boolean {
+ const regex = new RegExp(`^${IDENTIFIER_PATH_PATTERN}$`);
+ return regex.test(id);
+}
+
+function isValidPostId(id: string): boolean {
+ const regex = new RegExp(`^${ID_PATH_PATTERN}$`);
+ return regex.test(id);
+}
+
export function parseDeepLink(deepLinkUrl: string, asServer = false): DeepLinkWithData {
try {
const url = removeProtocol(deepLinkUrl);
const channelMatch = matchChannelDeeplink(url);
- if (channelMatch) {
+ if (channelMatch && isValidTeamName(channelMatch.params.teamName) && isValidIdentifierPathPattern(channelMatch.params.identifier)) {
const {params: {serverUrl, teamName, path, identifier}} = channelMatch;
if (path === 'channels') {
- return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}};
+ return {type: DeepLink.Channel, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, channelName: identifier}};
}
if (path === 'messages') {
if (identifier.startsWith('@')) {
- return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl, teamName, userName: identifier.substring(1)}};
+ return {type: DeepLink.DirectMessage, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, userName: identifier.substring(1)}};
}
- return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl, teamName, channelName: identifier}};
+ return {type: DeepLink.GroupMessage, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, channelName: identifier}};
}
}
const permalinkMatch = matchPermalinkDeeplink(url);
- if (permalinkMatch) {
+ if (permalinkMatch && isValidTeamName(permalinkMatch.params.teamName) && isValidPostId(permalinkMatch.params.postId)) {
const {params: {serverUrl, teamName, postId}} = permalinkMatch;
- return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl, teamName, postId}};
+ return {type: DeepLink.Permalink, url: deepLinkUrl, data: {serverUrl: serverUrl.join('/'), teamName, postId}};
}
if (asServer) {
diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock
index b2aeac871..31bf79f48 100644
--- a/fastlane/Gemfile.lock
+++ b/fastlane/Gemfile.lock
@@ -16,17 +16,17 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
- aws-partitions (1.966.0)
- aws-sdk-core (3.201.5)
+ aws-partitions (1.973.0)
+ aws-sdk-core (3.204.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
- aws-sdk-kms (1.88.0)
- aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sdk-kms (1.90.0)
+ aws-sdk-core (~> 3, >= 3.203.0)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.159.0)
- aws-sdk-core (~> 3, >= 3.201.0)
+ aws-sdk-s3 (1.161.0)
+ aws-sdk-core (~> 3, >= 3.203.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.9.1)
@@ -188,8 +188,7 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.3.5)
- strscan
+ rexml (3.3.7)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
@@ -202,7 +201,6 @@ GEM
simctl (1.6.10)
CFPropertyList
naturally
- strscan (3.1.0)
terminal-notifier (2.0.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
index 66c38346d..e440ac608 100644
--- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
+++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
@@ -69,6 +69,10 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
return SplitView.isRunningInSplitView()
}
+ fun getWindowDimensions(): WritableMap? {
+ return SplitView.getWindowDimensions()
+ }
+
fun unlockOrientation() {}
fun lockPortrait() {}
diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/enums/Events.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/enums/Events.kt
index 076400968..9bb7ad8bb 100644
--- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/enums/Events.kt
+++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/enums/Events.kt
@@ -2,5 +2,6 @@ package com.mattermost.rnutils.enums
enum class Events(val event: String) {
SAVE_ERROR_EVENT("SaveError"),
- SPLIT_VIEW_CHANGED("SplitViewChanged")
+ SPLIT_VIEW_CHANGED("SplitViewChanged"),
+ DIMENSIONS_CHANGED("DimensionsChanged")
}
diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/FoldableObserver.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/FoldableObserver.kt
index e7faf6217..9a65d905a 100644
--- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/FoldableObserver.kt
+++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/FoldableObserver.kt
@@ -1,6 +1,7 @@
package com.mattermost.rnutils.helpers
import android.app.Activity
+import android.graphics.Rect
import androidx.window.core.layout.WindowHeightSizeClass
import androidx.window.core.layout.WindowSizeClass
import androidx.window.core.layout.WindowWidthSizeClass
@@ -19,6 +20,7 @@ class FoldableObserver(activity: Activity) {
private lateinit var observable: Observable
var isDeviceFolded: Boolean = false
private val activityRef = WeakReference(activity)
+ private var windowBounds: Rect? = null
companion object {
private var instance: FoldableObserver? = null
@@ -43,6 +45,7 @@ class FoldableObserver(activity: Activity) {
val activity = activityRef.get() ?: return
observable = WindowInfoTracker.getOrCreate(activity)
.windowLayoutInfoObservable(activity)
+ this.windowBounds = getWindowSize()
}
fun onStart() {
@@ -52,6 +55,7 @@ class FoldableObserver(activity: Activity) {
disposable = observable.observeOn(AndroidSchedulers.mainThread())
.subscribe { layoutInfo ->
setIsDeviceFolded(layoutInfo)
+ handleWindowLayoutInfo(layoutInfo)
SplitView.setDeviceFolded()
}
}
@@ -78,11 +82,43 @@ class FoldableObserver(activity: Activity) {
}
}
+ private fun handleWindowLayoutInfo(windowLayoutInfo: WindowLayoutInfo) {
+ val bounds = getWindowSize()
+
+ if (bounds?.width() != windowBounds?.width()) {
+ // emit the dimensions changed event
+ windowBounds = bounds
+ SplitView.emitDimensionsChanged()
+ }
+ }
+
+
+ private fun getWindowSize(): Rect? {
+ val activity = activityRef.get() ?: return null
+ val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
+ val bounds = metrics.bounds
+ val widthPx = bounds.width()
+ val heightPx = bounds.height()
+
+ // Get the screen density (scale factor)
+ val displayMetrics = activity.resources.displayMetrics
+ val density = displayMetrics.density
+
+ // Adjust dimensions based on the scale factor (density)
+ val widthDp = (widthPx / density).toInt()
+ val heightDp = (heightPx / density).toInt()
+ return Rect(0, 0, widthDp, heightDp)
+ }
+
+ fun getWindowDimensions(): Rect? {
+ return this.windowBounds;
+ }
+
fun isCompactView(): Boolean {
val activity = activityRef.get() ?: return false
- val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
- val width = metrics.bounds.width()
- val height = metrics.bounds.height()
+ val bounds = getWindowSize()
+ val width = bounds?.width() ?: 0
+ val height = bounds?.height() ?: 0
val density = activity.resources.displayMetrics.density
val windowSizeClass = WindowSizeClass.compute(width / density, height / density)
val widthWindowSizeClass = windowSizeClass.windowWidthSizeClass
diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/SplitView.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/SplitView.kt
index cee7c3583..317b9a4a9 100644
--- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/SplitView.kt
+++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/helpers/SplitView.kt
@@ -33,7 +33,6 @@ class SplitView {
return isLargeScreen || isSmallestWidthLarge || isNotPhone
}
-
fun setDeviceFolded() {
val map = getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
RNUtilsModuleImpl.sendJSEvent(Events.SPLIT_VIEW_CHANGED.event, map)
@@ -57,5 +56,28 @@ class SplitView {
fun isRunningInSplitView(): WritableMap? {
return getSplitViewResults(FoldableObserver.getInstance()?.isDeviceFolded == true)
}
+
+ fun getWindowDimensions(): WritableMap? {
+ if (context?.currentActivity != null) {
+ val map = Arguments.createMap()
+ val bounds = FoldableObserver.getInstance()?.getWindowDimensions()
+ if (bounds != null) {
+ map.putInt("width", bounds.width())
+ map.putInt("height", bounds.height())
+ } else {
+ map.putInt("width", 0)
+ map.putInt("height", 0)
+ }
+
+ return map
+ }
+
+ return null
+ }
+
+ fun emitDimensionsChanged() {
+ val map = getWindowDimensions()
+ RNUtilsModuleImpl.sendJSEvent(Events.DIMENSIONS_CHANGED.event, map)
+ }
}
}
diff --git a/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt b/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt
index 2c9716f20..412265c29 100644
--- a/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt
+++ b/libraries/@mattermost/rnutils/android/src/newarch/RNUtilsModule.kt
@@ -27,6 +27,8 @@ class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSp
override fun isRunningInSplitView(): WritableMap? = implementation.isRunningInSplitView()
+ override fun getWindowDimensions(): WritableMap? = implementation.getWindowDimensions()
+
override fun unlockOrientation() {
implementation.unlockOrientation()
}
diff --git a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
index 861d03503..2ece72b4d 100644
--- a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
+++ b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
@@ -39,6 +39,11 @@ class RNUtilsModule(context: ReactApplicationContext) :
return implementation.isRunningInSplitView()
}
+ @ReactMethod(isBlockingSynchronousMethod = true)
+ fun getWindowDimensions(): WritableMap? {
+ return implementation.getWindowDimensions()
+ }
+
@ReactMethod
fun unlockOrientation() {
implementation.unlockOrientation()
diff --git a/libraries/@mattermost/rnutils/ios/Extensions/RNUtilsWrapper+Events.swift b/libraries/@mattermost/rnutils/ios/Extensions/RNUtilsWrapper+Events.swift
index 605170027..9d50deac3 100644
--- a/libraries/@mattermost/rnutils/ios/Extensions/RNUtilsWrapper+Events.swift
+++ b/libraries/@mattermost/rnutils/ios/Extensions/RNUtilsWrapper+Events.swift
@@ -3,6 +3,7 @@ import Foundation
extension RNUtilsWrapper {
enum Event: String, CaseIterable {
case SplitViewChanged
+ case DimensionsChanged
}
@objc
diff --git a/libraries/@mattermost/rnutils/ios/RNUtils.mm b/libraries/@mattermost/rnutils/ios/RNUtils.mm
index 97558b5b4..b0ccfd68e 100644
--- a/libraries/@mattermost/rnutils/ios/RNUtils.mm
+++ b/libraries/@mattermost/rnutils/ios/RNUtils.mm
@@ -72,6 +72,10 @@ RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(isRunningInSplitView, NSDictionary*, isSpl
return [wrapper isRunningInSplitView];
}
+RCT_REMAP_BLOCKING_SYNCHRONOUS_METHOD(getWindowDimensions, NSDictionary*, windowDimensions) {
+ return [wrapper getWindowDimensions];
+}
+
RCT_REMAP_METHOD(unlockOrientation, unlock) {
[wrapper unlockOrientation];
}
@@ -158,6 +162,9 @@ RCT_EXPORT_METHOD(saveFile:(NSString *)filePath
return [wrapper isRunningInSplitView];
}
+- (NSDictionary *)getWindowDimensions {
+ return [wrapper getWindowDimensions];
+}
- (void)lockPortrait {
[wrapper lockOrientation];
diff --git a/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift b/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift
index 0f00d1457..21a927a08 100644
--- a/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift
+++ b/libraries/@mattermost/rnutils/ios/RNUtilsWrapper.swift
@@ -22,6 +22,11 @@ import React
return sharedDirectory.appendingPathComponent("databases").path
}
+ func getWindowSize() -> (CGSize?, CGSize?) {
+ guard let w = UIApplication.shared.delegate?.window, let window = w else { return (nil, nil) }
+ return (window.screen.bounds.size, window.frame.size)
+ }
+
func isRunningInFullScreen() -> Bool {
guard let w = UIApplication.shared.delegate?.window, let window = w else { return false }
let screenSize = window.screen.bounds.size.width
@@ -30,16 +35,37 @@ import React
return shouldBeConsideredFullScreen
}
- @objc public func captureEvents() {
- NotificationCenter.default.addObserver(self,
- selector: #selector(isSplitView), name: NSNotification.Name.RCTUserInterfaceStyleDidChange,
- object: nil)
+ func isRunningInFullScreen(screen: CGSize?, frame: CGSize?) -> Bool {
+ guard let screenSize = screen?.width,
+ let frameSize = frame?.width else {return false}
+ let shouldBeConsideredFullScreen = frameSize >= (screenSize * 0.6)
+ return shouldBeConsideredFullScreen
}
- @objc func isSplitView() {
+ @objc public func captureEvents() {
+ DispatchQueue.main.async {
+ guard let w = UIApplication.shared.delegate?.window, let window = w else { return }
+ window.addObserver(self, forKeyPath: "frame", options: .new, context: nil)
+ }
+ }
+
+ override public func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
+ if keyPath == "frame" {
+ let (screen, frame) = getWindowSize()
+ guard let screen = screen, let frame = frame else {return}
+ isSplitView(screen: screen, frame: frame)
+
+ delegate?.sendEvent(name: "DimensionsChanged", result: [
+ "width": frame.width,
+ "height": frame.height
+ ])
+ }
+ }
+
+ @objc func isSplitView(screen: CGSize, frame: CGSize) {
if UIDevice.current.userInterfaceIdiom == .pad {
delegate?.sendEvent(name: "SplitViewChanged", result: [
- "isSplitView": !isRunningInFullScreen(),
+ "isSplit": !isRunningInFullScreen(screen: screen, frame: frame),
"isTablet": UIDevice.current.userInterfaceIdiom == .pad,
])
}
@@ -203,6 +229,28 @@ import React
]
}
+ @objc public func getWindowDimensions() -> Dictionary {
+ let queue = DispatchQueue.main
+ let group = DispatchGroup()
+ var dimensions = [
+ "width": 0.0,
+ "height": 0.0
+ ]
+ group.enter()
+ queue.async(group: group) { [weak self] in
+ if let (_, frame) = self?.getWindowSize(),
+ let frame = frame {
+ dimensions = [
+ "width": frame.width,
+ "height": frame.height
+ ]
+ }
+ group.leave()
+ }
+ group.wait()
+ return dimensions
+ }
+
@objc public func unlockOrientation() {
DispatchQueue.main.async {
OrientationManager.shared.unlockOrientation()
diff --git a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
index 339e16d17..a9a2ce5a2 100644
--- a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
+++ b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
@@ -3,7 +3,7 @@
import {type TurboModule, TurboModuleRegistry} from 'react-native';
-import type {Int32, UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes';
+import type {Double, Int32, UnsafeObject} from 'react-native/Libraries/Types/CodegenTypes';
export type SplitView = Readonly<{
isSplit: boolean;
@@ -36,6 +36,11 @@ type Constants = Readonly<{
}>;
}>
+export type WindowDimensionsChanged = Readonly<{
+ width: Double;
+ height: Double;
+}>
+
export interface Spec extends TurboModule {
readonly getConstants: () => Constants;
@@ -45,6 +50,7 @@ export interface Spec extends TurboModule {
getRealFilePath: (filePath: string) => Promise;
saveFile: (filePath: string) => Promise;
+ getWindowDimensions: () => WindowDimensionsChanged;
isRunningInSplitView: () => SplitView;
unlockOrientation: () => void;
lockPortrait: () => void;
diff --git a/libraries/@mattermost/rnutils/src/index.tsx b/libraries/@mattermost/rnutils/src/index.tsx
index 9ae35b612..e0d9e5f4a 100644
--- a/libraries/@mattermost/rnutils/src/index.tsx
+++ b/libraries/@mattermost/rnutils/src/index.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {NativeModules, Platform} from 'react-native';
-import type {Spec, SplitView} from './NativeRNUtils';
+import type {Spec, SplitView, WindowDimensionsChanged} from './NativeRNUtils';
const LINKING_ERROR =
'The package \'@mattermost/rnutils\' doesn\'t seem to be linked. Make sure: \n\n' +
@@ -26,5 +26,7 @@ const RNUtils = RNUtilsModule || new Proxy(
export type SplitViewResult = SplitView;
+export type WindowDimensions = WindowDimensionsChanged;
+
export default RNUtils;
diff --git a/package-lock.json b/package-lock.json
index cb9d55b81..ba5922715 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -62,7 +62,7 @@
"moment-timezone": "0.5.45",
"node-html-parser": "6.1.13",
"pako": "2.1.0",
- "path-to-regexp": "7.1.0",
+ "path-to-regexp": "8.1.0",
"react": "18.2.0",
"react-freeze": "1.0.4",
"react-intl": "6.6.8",
@@ -22634,9 +22634,9 @@
}
},
"node_modules/path-to-regexp": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-7.1.0.tgz",
- "integrity": "sha512-ZToe+MbUF4lBqk6dV8GKot4DKfzrxXsplOddH8zN3YK+qw9/McvP7+4ICjZvOne0jQhN4eJwHsX6tT0Ns19fvw==",
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.1.0.tgz",
+ "integrity": "sha512-Bqn3vc8CMHty6zuD+tG23s6v2kwxslHEhTj4eYaVKGIEB+YX/2wd0/rgXLFD9G9id9KCtbVy/3ZgmvZjpa0UdQ==",
"license": "MIT",
"engines": {
"node": ">=16"
diff --git a/package.json b/package.json
index e83cd0038..703548cda 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"moment-timezone": "0.5.45",
"node-html-parser": "6.1.13",
"pako": "2.1.0",
- "path-to-regexp": "7.1.0",
+ "path-to-regexp": "8.1.0",
"react": "18.2.0",
"react-freeze": "1.0.4",
"react-intl": "6.6.8",
diff --git a/patches/expo-video-thumbnails+8.0.0.patch b/patches/expo-video-thumbnails+8.0.0.patch
new file mode 100644
index 000000000..bfe3ce630
--- /dev/null
+++ b/patches/expo-video-thumbnails+8.0.0.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/expo-video-thumbnails/ios/VideoThumbnailsModule.swift b/node_modules/expo-video-thumbnails/ios/VideoThumbnailsModule.swift
+index 2b01a1d..78c7ffa 100644
+--- a/node_modules/expo-video-thumbnails/ios/VideoThumbnailsModule.swift
++++ b/node_modules/expo-video-thumbnails/ios/VideoThumbnailsModule.swift
+@@ -7,7 +7,7 @@ public class VideoThumbnailsModule: Module {
+ public func definition() -> ModuleDefinition {
+ Name("ExpoVideoThumbnails")
+
+- AsyncFunction("getThumbnail", getVideoThumbnail).runOnQueue(.main)
++ AsyncFunction("getThumbnail", getVideoThumbnail).runOnQueue(.global())
+ }
+
+ internal func getVideoThumbnail(sourceFilename: URL, options: VideoThumbnailsOptions) throws -> [String: Any] {