From 394da4487c0f7148549514de1a78c72649929fc1 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Fri, 13 Sep 2024 04:46:15 +0800 Subject: [PATCH] Various Fixes (#8212) --- .../channel_bookmark/channel_bookmark.tsx | 28 ++++---- app/hooks/device.ts | 19 +++++- app/screens/gallery/index.tsx | 6 +- app/screens/gallery/video_renderer/index.tsx | 17 ++--- app/screens/gallery/viewer/index.tsx | 2 +- app/utils/deep_link/index.ts | 65 ++++++++++++++----- fastlane/Gemfile.lock | 16 ++--- .../mattermost/rnutils/RNUtilsModuleImpl.kt | 4 ++ .../com/mattermost/rnutils/enums/Events.kt | 3 +- .../rnutils/helpers/FoldableObserver.kt | 42 +++++++++++- .../mattermost/rnutils/helpers/SplitView.kt | 24 ++++++- .../android/src/newarch/RNUtilsModule.kt | 2 + .../android/src/oldarch/RNUtilsModule.kt | 5 ++ .../Extensions/RNUtilsWrapper+Events.swift | 1 + libraries/@mattermost/rnutils/ios/RNUtils.mm | 7 ++ .../rnutils/ios/RNUtilsWrapper.swift | 60 +++++++++++++++-- .../@mattermost/rnutils/src/NativeRNUtils.ts | 8 ++- libraries/@mattermost/rnutils/src/index.tsx | 4 +- package-lock.json | 8 +-- package.json | 2 +- patches/expo-video-thumbnails+8.0.0.patch | 13 ++++ 21 files changed, 259 insertions(+), 77 deletions(-) create mode 100644 patches/expo-video-thumbnails+8.0.0.patch 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] {