Extra keyboard (#8348)

* Add support to use the keyboard area with a component

* fix import

* add missing providers to involved screens

* Change the way we handle the keyboard to allow using custom components in that area

* review feedback
This commit is contained in:
Elias Nahum 2024-11-29 11:58:13 +08:00 committed by GitHub
parent 732b17a75c
commit d25c6fe245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 427 additions and 1658 deletions

View file

@ -1,16 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import KeyboardTrackingView, {type KeyboardTrackingViewRef} from '@mattermost/keyboard-tracker';
import React, {type RefObject, useEffect, useState} from 'react';
import {Platform} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import React, {useEffect, useState} from 'react';
import Autocomplete from '@components/autocomplete';
import {View as ViewConstants} from '@constants';
import {ExtraKeyboard} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useAutocompleteDefaultAnimatedValues} from '@hooks/autocomplete';
import {useIsTablet, useKeyboardHeight} from '@hooks/device';
import {useKeyboardHeight} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import Archived from './archived';
@ -20,7 +17,6 @@ import ReadOnly from './read_only';
const AUTOCOMPLETE_ADJUST = -5;
type Props = {
testID?: string;
accessoriesContainerID?: string;
canPost: boolean;
channelId: string;
channelIsArchived?: boolean;
@ -30,18 +26,13 @@ type Props = {
isSearch?: boolean;
message?: string;
rootId?: string;
scrollViewNativeID?: string;
keyboardTracker: RefObject<KeyboardTrackingViewRef>;
containerHeight: number;
isChannelScreen: boolean;
canShowPostPriority?: boolean;
}
const {KEYBOARD_TRACKING_OFFSET} = ViewConstants;
function PostDraft({
testID,
accessoriesContainerID,
canPost,
channelId,
channelIsArchived,
@ -51,8 +42,6 @@ function PostDraft({
isSearch,
message = '',
rootId = '',
scrollViewNativeID,
keyboardTracker,
containerHeight,
isChannelScreen,
canShowPostPriority,
@ -61,9 +50,7 @@ function PostDraft({
const [cursorPosition, setCursorPosition] = useState(message.length);
const [postInputTop, setPostInputTop] = useState(0);
const [isFocused, setIsFocused] = useState(false);
const isTablet = useIsTablet();
const keyboardHeight = useKeyboardHeight(keyboardTracker);
const insets = useSafeAreaInsets();
const keyboardHeight = useKeyboardHeight();
const headerHeight = useDefaultHeaderHeight();
const serverUrl = useServerUrl();
@ -73,12 +60,7 @@ function PostDraft({
setCursorPosition(message.length);
}, [channelId, rootId]);
const keyboardAdjustment = (isTablet && isChannelScreen) ? KEYBOARD_TRACKING_OFFSET : 0;
const insetsAdjustment = (isTablet && isChannelScreen) ? 0 : insets.bottom;
const autocompletePosition = AUTOCOMPLETE_ADJUST + Platform.select({
ios: (keyboardHeight ? keyboardHeight - keyboardAdjustment : (postInputTop + insetsAdjustment)),
default: postInputTop + insetsAdjustment,
});
const autocompletePosition = AUTOCOMPLETE_ADJUST + keyboardHeight + postInputTop;
const autocompleteAvailableSpace = containerHeight - autocompletePosition - (isChannelScreen ? headerHeight : 0);
const [animatedAutocompletePosition, animatedAutocompleteAvailableSpace] = useAutocompleteDefaultAnimatedValues(autocompletePosition, autocompleteAvailableSpace);
@ -136,26 +118,11 @@ function PostDraft({
/>
) : null;
if (Platform.OS === 'android') {
return (
<>
{draftHandler}
{autoComplete}
</>
);
}
return (
<>
<KeyboardTrackingView
accessoriesContainerID={accessoriesContainerID}
ref={keyboardTracker}
scrollViewNativeID={scrollViewNativeID}
viewInitialOffsetY={isTablet && !rootId ? KEYBOARD_TRACKING_OFFSET : 0}
>
{draftHandler}
</KeyboardTrackingView>
{draftHandler}
{autoComplete}
<ExtraKeyboard/>
</>
);
}

View file

@ -14,6 +14,7 @@ import {
import {updateDraftMessage} from '@actions/local/draft';
import {userTyping} from '@actions/websocket/users';
import {Events, Screens} from '@constants';
import {useExtraKeyboardContext} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@ -121,6 +122,7 @@ export default function PostInput({
const style = getStyleSheet(theme);
const serverUrl = useServerUrl();
const managedConfig = useManagedConfig<ManagedConfig>();
const keyboardContext = useExtraKeyboardContext();
const [propagateValue, shouldProcessEvent] = useInputPropagation();
const lastTypingEventSent = useRef(0);
@ -145,13 +147,15 @@ export default function PostInput({
};
const onBlur = useCallback(() => {
keyboardContext?.registerTextInputBlur();
updateDraftMessage(serverUrl, channelId, rootId, value);
setIsFocused(false);
}, [channelId, rootId, value, setIsFocused]);
}, [keyboardContext, serverUrl, channelId, rootId, value, setIsFocused]);
const onFocus = useCallback(() => {
keyboardContext?.registerTextInputFocus();
setIsFocused(true);
}, [setIsFocused]);
}, [setIsFocused, keyboardContext]);
const checkMessageLength = useCallback((newValue: string) => {
const valueLength = newValue.trim().length;

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type ReactNode, useEffect, useMemo, useRef, useState} from 'react';
import React, {type ReactNode, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, Platform, type StyleProp, View, type ViewStyle, TouchableHighlight} from 'react-native';
@ -14,6 +14,7 @@ import SystemAvatar from '@components/system_avatar';
import SystemHeader from '@components/system_header';
import {POST_TIME_TO_FAIL} from '@constants/post';
import * as Screens from '@constants/screens';
import {useHideExtraKeyboardIfNeeded} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
@ -21,7 +22,6 @@ import PerformanceMetricsManager from '@managers/performance_metrics_manager';
import {openAsBottomSheet} from '@screens/navigation';
import {hasJumboEmojiOnly} from '@utils/emoji/helpers';
import {fromAutoResponder, isFromWebhook, isPostFailed, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import Avatar from './avatar';
@ -176,7 +176,7 @@ const Post = ({
return false;
}, [customEmojiNames, post.message]);
const handlePostPress = () => {
const handlePostPress = useCallback(() => {
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
showPermalink(serverUrl, '', post.id);
return;
@ -195,19 +195,20 @@ const Post = ({
setTimeout(() => {
pressDetected.current = false;
}, 300);
};
}, [
hasBeenDeleted, isAutoResponder, isEphemeral,
isPendingOrFailed, isSystemPost, location, serverUrl, post,
]);
const handlePress = preventDoubleTap(() => {
const handlePress = useHideExtraKeyboardIfNeeded(() => {
pressDetected.current = true;
if (post) {
Keyboard.dismiss();
setTimeout(handlePostPress, 300);
}
});
}, [handlePostPress, post]);
const showPostOptions = () => {
const showPostOptions = useHideExtraKeyboardIfNeeded(() => {
if (!post) {
return;
}
@ -231,7 +232,11 @@ const Post = ({
title,
props: passProps,
});
};
}, [
canDelete, hasBeenDeleted, intl,
isEphemeral, isPendingOrFailed, isTablet, isSystemPost,
location, post, serverUrl, showAddReaction, theme,
]);
const [, rerender] = useState(false);
useEffect(() => {

View file

@ -5,13 +5,10 @@ export const MAX_MESSAGE_LENGTH_FALLBACK = 4000;
export const DEFAULT_SERVER_MAX_FILE_SIZE = 50 * 1024 * 1024;// 50 Mb
export const ICON_SIZE = 24;
export const TYPING_HEIGHT = 16;
export const ACCESSORIES_CONTAINER_NATIVE_ID = 'channelAccessoriesContainer';
export const THREAD_ACCESSORIES_CONTAINER_NATIVE_ID = 'threadAccessoriesContainer';
export const NOTIFY_ALL_MEMBERS = 5;
export default {
ACCESSORIES_CONTAINER_NATIVE_ID,
DEFAULT_SERVER_MAX_FILE_SIZE,
ICON_SIZE,
MAX_MESSAGE_LENGTH_FALLBACK,

View file

@ -0,0 +1,183 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {createContext, useCallback, useContext, useEffect, useState} from 'react';
import {Keyboard, Platform} from 'react-native';
import Animated, {KeyboardState, useAnimatedKeyboard, useAnimatedStyle, useDerivedValue, withTiming} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {Screens} from '@constants';
import {useIsTablet} from '@hooks/device';
import NavigationStore from '@store/navigation_store';
import {preventDoubleTap} from '@utils/tap';
import type {AvailableScreens} from '@typings/screens/navigation';
export type ExtraKeyboardContextProps = {
isExtraKeyboardVisible: boolean;
component: React.ReactElement|null;
showExtraKeyboard: (component: React.ReactElement|null) => void;
hideExtraKeyboard: () => void;
registerTextInputFocus: () => void;
registerTextInputBlur: () => void;
};
// This is based on the size of the tab bar
const KEYBOARD_OFFSET = -77;
export const ExtraKeyboardContext = createContext<ExtraKeyboardContextProps|undefined>(undefined);
const useOffetForCurrentScreen = (): number => {
const [screen, setScreen] = useState<AvailableScreens|undefined>();
const [offset, setOffset] = useState(0);
const isTablet = useIsTablet();
useEffect(() => {
const sub = NavigationStore.getSubject();
const s = sub.subscribe(setScreen);
return () => s.unsubscribe();
}, []);
useEffect(() => {
if (isTablet && screen === Screens.HOME) {
setOffset(KEYBOARD_OFFSET);
}
}, [isTablet, screen]);
return offset;
};
export const ExtraKeyboardProvider = (({children}: {children: React.ReactElement|React.ReactElement[]}) => {
const [isExtraKeyboardVisible, setExtraKeyboardVisible] = useState(false);
const [component, setComponent] = useState<React.ReactElement|null>(null);
const [isTextInputFocused, setIsTextInputFocused] = useState(false);
const showExtraKeyboard = useCallback((newComponent: React.ReactElement|null) => {
setExtraKeyboardVisible(true);
setComponent(newComponent);
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
}, []);
const hideExtraKeyboard = useCallback(() => {
setExtraKeyboardVisible(false);
setComponent(null);
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
}, []);
const registerTextInputFocus = useCallback(() => {
// If the extra keyboard is opened if we don't do this
// we get a glitch in the UI that will animate the extra keyboard down
// and immediately bring the keyboard, by doing this
// we delay hidding the extra keyboard, so that there is no animation glitch
setIsTextInputFocused(true);
setTimeout(() => {
setExtraKeyboardVisible(false);
}, 400);
}, []);
const registerTextInputBlur = useCallback(() => {
setIsTextInputFocused(false);
}, []);
useEffect(() => {
const keyboardHideListener = Keyboard.addListener('keyboardDidHide', () => {
if (isTextInputFocused) {
setExtraKeyboardVisible(false);
}
});
return () => keyboardHideListener.remove();
}, [isTextInputFocused]);
return (
<ExtraKeyboardContext.Provider
value={{
isExtraKeyboardVisible,
component,
showExtraKeyboard,
hideExtraKeyboard,
registerTextInputBlur,
registerTextInputFocus,
}}
>
{children}
</ExtraKeyboardContext.Provider>
);
});
export const useExtraKeyboardContext = (): ExtraKeyboardContextProps|undefined => {
const context = useContext(ExtraKeyboardContext);
if (!context) {
throw new Error('useExtraKeyboardContext must be used within a ExtraKeyboardProvider');
}
return context;
};
export const useHideExtraKeyboardIfNeeded = (callback: (...args: any) => void, dependencies: React.DependencyList = []) => {
const keyboardContext = useExtraKeyboardContext();
return useCallback(preventDoubleTap((...args: any) => {
if (keyboardContext?.isExtraKeyboardVisible) {
keyboardContext.hideExtraKeyboard();
/*
/* At this point the early return is commented
/* Based on the UX we actually want to have
/* we can uncoment this and reaturn as early
/* as the custom keyboard is hidden
*/
// return;
}
if (Keyboard.isVisible()) {
Keyboard.dismiss();
}
callback(...args);
}), [keyboardContext, ...dependencies]);
};
export const ExtraKeyboard = () => {
const keyb = useAnimatedKeyboard({isStatusBarTranslucentAndroid: true});
const defaultKeyboardHeight = Platform.select({ios: 291, default: 240});
const context = useExtraKeyboardContext();
const insets = useSafeAreaInsets();
const offset = useOffetForCurrentScreen();
const maxKeyboardHeight = useDerivedValue(() => {
if (keyb.state.value === KeyboardState.OPEN) {
const keyboardOffset = keyb.height.value < 70 ? 0 : offset; // When using a hw keyboard
return keyb.height.value + keyboardOffset;
}
return defaultKeyboardHeight;
});
const animatedStyle = useAnimatedStyle(() => {
let height = keyb.height.value + offset;
if (keyb.height.value < 70) {
height = 0; // When using a hw keyboard
}
if (context?.isExtraKeyboardVisible) {
height = withTiming(maxKeyboardHeight.value, {duration: 250});
} else if (keyb.state.value === KeyboardState.CLOSED || keyb.state.value === KeyboardState.UNKNOWN) {
height = withTiming(0, {duration: 250});
}
return {
height,
marginBottom: withTiming((keyb.state.value === KeyboardState.CLOSED || keyb.state.value === KeyboardState.CLOSING || keyb.state.value === KeyboardState.UNKNOWN) ? insets.bottom : 0, {duration: 250}),
};
}, [context, insets.bottom, offset]);
return (
<Animated.View style={animatedStyle}>
{context?.isExtraKeyboardVisible && context.component}
</Animated.View>
);
};

View file

@ -3,12 +3,11 @@
import RNUtils, {type WindowDimensions} from '@mattermost/rnutils';
import React, {type RefObject, useEffect, useRef, useState, useContext} from 'react';
import {AppState, DeviceEventEmitter, Keyboard, NativeEventEmitter, Platform, View} from 'react-native';
import {AppState, 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';
import type {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
const utilsEmitter = new NativeEventEmitter(RNUtils);
@ -51,43 +50,14 @@ export function useIsTablet() {
return isTablet && !isSplit;
}
export function useKeyboardHeightWithDuration(keyboardTracker?: RefObject<KeyboardTrackingViewRef>) {
export function useKeyboardHeightWithDuration() {
const [keyboardHeight, setKeyboardHeight] = useState({height: 0, duration: 0});
const updateTimeout = useRef<NodeJS.Timeout | null>(null);
const insets = useSafeAreaInsets();
// This is a magic number. With tracking view, to properly get the final position, this had to be added.
const KEYBOARD_TRACKINGVIEW_SEPARATION = 4;
const updateValue = (height: number, duration: number) => {
if (updateTimeout.current != null) {
clearTimeout(updateTimeout.current);
updateTimeout.current = null;
}
updateTimeout.current = setTimeout(() => {
setKeyboardHeight({height, duration});
updateTimeout.current = null;
}, 200);
};
useEffect(() => {
const show = Keyboard.addListener(Platform.select({ios: 'keyboardWillShow', default: 'keyboardDidShow'}), async (event) => {
if (!keyboardTracker?.current) {
setKeyboardHeight({height: event.endCoordinates.height, duration: event.duration});
}
});
const tracker = DeviceEventEmitter.addListener('MattermostKeyboardTrackerView', (event: KeyboardWillShowEventData) => {
const props = event.nativeEvent;
if (keyboardTracker?.current) {
if (props.keyboardHeight) {
updateValue((props.trackingViewHeight + props.keyboardHeight) - KEYBOARD_TRACKINGVIEW_SEPARATION, props.animationDuration);
} else {
updateValue((props.trackingViewHeight + insets.bottom) - KEYBOARD_TRACKINGVIEW_SEPARATION, props.animationDuration);
}
} else {
setKeyboardHeight({height: props.keyboardFrameEndHeight, duration: props.animationDuration});
}
setKeyboardHeight({height: event.endCoordinates.height, duration: event.duration});
});
const hide = Keyboard.addListener(Platform.select({ios: 'keyboardWillHide', default: 'keyboardDidHide'}), (event) => {
@ -101,15 +71,14 @@ export function useKeyboardHeightWithDuration(keyboardTracker?: RefObject<Keyboa
return () => {
show.remove();
hide.remove();
tracker.remove();
};
}, [keyboardTracker?.current && insets.bottom]);
}, [insets.bottom]);
return keyboardHeight;
}
export function useKeyboardHeight(keyboardTracker?: RefObject<KeyboardTrackingViewRef>) {
const {height} = useKeyboardHeightWithDuration(keyboardTracker);
export function useKeyboardHeight() {
const {height} = useKeyboardHeightWithDuration();
return height;
}
@ -126,7 +95,7 @@ export function useViewPosition(viewRef: RefObject<View>, deps: React.Dependency
}
});
}
}, [...deps, isTablet, height]);
}, [...deps, isTablet, height, viewRef, modalPosition]);
return modalPosition;
}

View file

@ -1,52 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {type KeyboardTrackingViewRef} from '@mattermost/keyboard-tracker';
import {type RefObject, useEffect, useRef} from 'react';
import {Navigation} from 'react-native-navigation';
import NavigationStore from '@store/navigation_store';
export const useKeyboardTrackingPaused = (keyboardTrackingRef: RefObject<KeyboardTrackingViewRef>, trackerId: string, screens: string[]) => {
const isPostDraftPaused = useRef(false);
useEffect(() => {
if (keyboardTrackingRef.current) {
keyboardTrackingRef.current.resumeTracking(trackerId);
}
}, []);
useEffect(() => {
const onCommandComplete = () => {
const id = NavigationStore.getVisibleScreen();
if (screens.includes(id) && isPostDraftPaused.current) {
isPostDraftPaused.current = false;
keyboardTrackingRef.current?.resumeTracking(trackerId);
}
};
const commandListener = Navigation.events().registerCommandListener(() => {
setTimeout(() => {
const visibleScreen = NavigationStore.getVisibleScreen();
if (!isPostDraftPaused.current && !screens.includes(visibleScreen)) {
isPostDraftPaused.current = true;
keyboardTrackingRef.current?.pauseTracking(trackerId);
}
});
});
const commandCompletedListener = Navigation.events().registerCommandCompletedListener(() => {
onCommandComplete();
});
const popListener = Navigation.events().registerScreenPoppedListener(() => {
onCommandComplete();
});
return () => {
commandListener.remove();
commandCompletedListener.remove();
popListener.remove();
};
}, [trackerId]);
};

View file

@ -1,22 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {type KeyboardTrackingViewRef} from 'libraries/@mattermost/keyboard-tracker/src';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import RNUtils from '@mattermost/rnutils';
import React, {useCallback, useEffect, useState} from 'react';
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {type Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
import {storeLastViewedChannelIdAndServer, removeLastViewedChannelIdAndServer} from '@actions/app/global';
import FloatingCallContainer from '@calls/components/floating_call_container';
import FreezeScreen from '@components/freeze_screen';
import PostDraft from '@components/post_draft';
import {Screens} from '@constants';
import {ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import {useChannelSwitch} from '@hooks/channel_switch';
import {useIsTablet} from '@hooks/device';
import {useDefaultHeaderHeight} from '@hooks/header';
import {useKeyboardTrackingPaused} from '@hooks/keyboard_tracking';
import {useTeamSwitch} from '@hooks/team_switch';
import {popTopScreen} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
@ -45,7 +44,6 @@ type ChannelProps = {
};
const edges: Edge[] = ['left', 'right'];
const trackKeyboardForScreens = [Screens.HOME, Screens.CHANNEL];
const styles = StyleSheet.create({
flex: {
@ -75,16 +73,28 @@ const Channel = ({
const switchingTeam = useTeamSwitch();
const switchingChannels = useChannelSwitch();
const defaultHeight = useDefaultHeaderHeight();
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
const [containerHeight, setContainerHeight] = useState(0);
const shouldRender = !switchingTeam && !switchingChannels && shouldRenderPosts && Boolean(channelId);
const handleBack = useCallback(() => {
popTopScreen(componentId);
}, [componentId]);
useKeyboardTrackingPaused(postDraftRef, channelId, trackKeyboardForScreens);
useAndroidHardwareBackHandler(componentId, handleBack);
useEffect(() => {
const listener = {
componentDidAppear: () => {
RNUtils.setSoftKeyboardToAdjustNothing();
},
componentDidDisappear: () => {
RNUtils.setSoftKeyboardToAdjustResize();
},
};
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId!);
return () => unsubscribe.remove();
}, []);
const marginTop = defaultHeight + (isTablet ? 0 : -insets.top);
useEffect(() => {
// This is done so that the header renders
@ -132,7 +142,7 @@ const Channel = ({
shouldRenderBookmarks={shouldRender}
/>
{shouldRender &&
<>
<ExtraKeyboardProvider>
<View style={[styles.flex, {marginTop}]}>
<ChannelPostList
channelId={channelId}
@ -141,15 +151,12 @@ const Channel = ({
</View>
<PostDraft
channelId={channelId}
keyboardTracker={postDraftRef}
scrollViewNativeID={channelId}
accessoriesContainerID={ACCESSORIES_CONTAINER_NATIVE_ID}
testID='channel.post_draft'
containerHeight={containerHeight}
isChannelScreen={true}
canShowPostPriority={true}
/>
</>
</ExtraKeyboardProvider>
}
{showFloatingCallContainer && shouldRender &&
<FloatingCallContainer

View file

@ -31,7 +31,7 @@ type Props = {
shouldShowJoinLeaveMessages: boolean;
}
const edges: Edge[] = ['bottom'];
const edges: Edge[] = [];
const styles = StyleSheet.create({
flex: {flex: 1},
containerStyle: {paddingTop: 12},

View file

@ -57,7 +57,7 @@ const updateTimezoneIfNeeded = async () => {
}
};
export default function HomeScreen(props: HomeProps) {
export function HomeScreen(props: HomeProps) {
const theme = useTheme();
const intl = useIntl();
const appState = useAppState();
@ -82,7 +82,7 @@ export default function HomeScreen(props: HomeProps) {
return () => {
listener.remove();
};
}, [intl.locale]);
}, [intl]);
useEffect(() => {
const leaveTeamListener = DeviceEventEmitter.addListener(Events.LEAVE_TEAM, (displayName: string) => {
@ -109,7 +109,7 @@ export default function HomeScreen(props: HomeProps) {
archivedChannelListener.remove();
crtToggledListener.remove();
};
}, [intl.locale]);
}, [intl]);
useEffect(() => {
if (appState === 'active') {
@ -191,3 +191,5 @@ export default function HomeScreen(props: HomeProps) {
</>
);
}
export default HomeScreen;

View file

@ -14,6 +14,7 @@ import DateSeparator from '@components/post_list/date_separator';
import PostWithChannelInfo from '@components/post_with_channel_info';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Events, Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useCollapsibleHeader} from '@hooks/header';
@ -162,7 +163,7 @@ const RecentMentionsScreen = ({appsEnabled, customEmojiNames, mentions, currentT
}, [appsEnabled, customEmojiNames]);
return (
<>
<ExtraKeyboardProvider>
<NavigationHeader
isLargeTitle={true}
showBackButton={false}
@ -200,7 +201,7 @@ const RecentMentionsScreen = ({appsEnabled, customEmojiNames, mentions, currentT
/>
</Animated.View>
</SafeAreaView>
</>
</ExtraKeyboardProvider>
);
};

View file

@ -15,6 +15,7 @@ import DateSeparator from '@components/post_list/date_separator';
import PostWithChannelInfo from '@components/post_with_channel_info';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Events, Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useCollapsibleHeader} from '@hooks/header';
@ -164,7 +165,7 @@ function SavedMessages({appsEnabled, posts, currentTimezone, customEmojiNames}:
}, [appsEnabled, currentTimezone, customEmojiNames, theme]);
return (
<>
<ExtraKeyboardProvider>
<NavigationHeader
isLargeTitle={true}
showBackButton={false}
@ -202,7 +203,7 @@ function SavedMessages({appsEnabled, posts, currentTimezone, customEmojiNames}:
/>
</Animated.View>
</SafeAreaView>
</>
</ExtraKeyboardProvider>
);
}

View file

@ -9,6 +9,7 @@ import NoResultsWithTerm from '@components/no_results_with_term';
import DateSeparator from '@components/post_list/date_separator';
import PostWithChannelInfo from '@components/post_with_channel_info';
import {Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useTheme} from '@context/theme';
import {convertSearchTermToRegex, parseSearchTerms} from '@utils/markdown';
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
@ -89,7 +90,7 @@ const PostResults = ({
default:
return null;
}
}, [appsEnabled, customEmojiNames, searchValue, matches]);
}, [currentTimezone, searchValue, matches, appsEnabled, customEmojiNames]);
const noResults = useMemo(() => (
<NoResultsWithTerm
@ -99,33 +100,35 @@ const PostResults = ({
), [searchValue]);
return (
<FlatList
ListHeaderComponent={
<FormattedText
style={styles.resultsNumber}
id='mobile.search.results'
defaultMessage='{count} search {count, plural, one {result} other {results}}'
values={{count: posts.length}}
/>
}
ListEmptyComponent={noResults}
contentContainerStyle={containerStyle}
data={orderedPosts}
indicatorStyle='black'
initialNumToRender={5}
<ExtraKeyboardProvider>
<FlatList
ListHeaderComponent={
<FormattedText
style={styles.resultsNumber}
id='mobile.search.results'
defaultMessage='{count} search {count, plural, one {result} other {results}}'
values={{count: posts.length}}
/>
}
ListEmptyComponent={noResults}
contentContainerStyle={containerStyle}
data={orderedPosts}
indicatorStyle='black'
initialNumToRender={5}
//@ts-expect-error key not defined in types
listKey={'posts'}
maxToRenderPerBatch={5}
nestedScrollEnabled={true}
refreshing={false}
removeClippedSubviews={true}
renderItem={renderItem}
scrollEventThrottle={16}
scrollToOverflowEnabled={true}
showsVerticalScrollIndicator={true}
testID='search_results.post_list.flat_list'
/>
//@ts-expect-error key not defined in types
listKey={'posts'}
maxToRenderPerBatch={5}
nestedScrollEnabled={true}
refreshing={false}
removeClippedSubviews={true}
renderItem={renderItem}
scrollEventThrottle={16}
scrollToOverflowEnabled={true}
showsVerticalScrollIndicator={true}
testID='search_results.post_list.flat_list'
/>
</ExtraKeyboardProvider>
);
};

View file

@ -15,6 +15,7 @@ import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import PostList from '@components/post_list';
import {Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
@ -315,7 +316,7 @@ function Permalink({
);
} else {
content = (
<>
<ExtraKeyboardProvider>
<View style={style.postList}>
<PostList
highlightedId={postId}
@ -345,7 +346,7 @@ function Permalink({
/>
</TouchableOpacity>
</View>
</>
</ExtraKeyboardProvider>
);
}

View file

@ -10,6 +10,7 @@ import Loading from '@components/loading';
import DateSeparator from '@components/post_list/date_separator';
import Post from '@components/post_list/post';
import {Events, Screens} from '@constants';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
@ -152,17 +153,19 @@ function SavedMessages({
style={styles.flex}
testID='pinned_messages.screen'
>
<FlatList
contentContainerStyle={data.length ? styles.list : [styles.empty]}
ListEmptyComponent={emptyList}
data={data}
onRefresh={handleRefresh}
refreshing={refreshing}
renderItem={renderItem}
scrollToOverflowEnabled={true}
onViewableItemsChanged={onViewableItemsChanged}
testID='pinned_messages.post_list.flat_list'
/>
<ExtraKeyboardProvider>
<FlatList
contentContainerStyle={data.length ? styles.list : [styles.empty]}
ListEmptyComponent={emptyList}
data={data}
onRefresh={handleRefresh}
refreshing={refreshing}
renderItem={renderItem}
scrollToOverflowEnabled={true}
onViewableItemsChanged={onViewableItemsChanged}
testID='pinned_messages.post_list.flat_list'
/>
</ExtraKeyboardProvider>
</SafeAreaView>
);
}

View file

@ -1,9 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import RNUtils from '@mattermost/rnutils';
import {uniqueId} from 'lodash';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import React, {useCallback, useEffect, useState} from 'react';
import {type LayoutChangeEvent, StyleSheet, View} from 'react-native';
import {Navigation} from 'react-native-navigation';
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
import {storeLastViewedThreadIdAndServer, removeLastViewedThreadIdAndServer} from '@actions/app/global';
@ -12,17 +14,15 @@ import FreezeScreen from '@components/freeze_screen';
import PostDraft from '@components/post_draft';
import RoundedHeaderContext from '@components/rounded_header_context';
import {Screens} from '@constants';
import {THREAD_ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
import useDidUpdate from '@hooks/did_update';
import {useKeyboardTrackingPaused} from '@hooks/keyboard_tracking';
import {popTopScreen, setButtons} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import NavigationStore from '@store/navigation_store';
import ThreadPostList from './thread_post_list';
import type {KeyboardTrackingViewRef} from '@mattermost/keyboard-tracker';
import type PostModel from '@typings/database/models/servers/post';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -37,7 +37,6 @@ type ThreadProps = {
};
const edges: Edge[] = ['left', 'right'];
const trackKeyboardForScreens = [Screens.THREAD];
const styles = StyleSheet.create({
flex: {flex: 1},
@ -52,16 +51,28 @@ const Thread = ({
isInACall,
showIncomingCalls,
}: ThreadProps) => {
const postDraftRef = useRef<KeyboardTrackingViewRef>(null);
const [containerHeight, setContainerHeight] = useState(0);
const close = useCallback(() => {
popTopScreen(componentId);
}, [componentId]);
useKeyboardTrackingPaused(postDraftRef, rootId, trackKeyboardForScreens);
useAndroidHardwareBackHandler(componentId, close);
useEffect(() => {
const listener = {
componentDidAppear: () => {
RNUtils.setSoftKeyboardToAdjustNothing();
},
componentDidDisappear: () => {
RNUtils.setSoftKeyboardToAdjustResize();
},
};
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId!);
return () => unsubscribe.remove();
}, []);
useEffect(() => {
if (isCRTEnabled && rootId) {
const id = `${componentId}-${rootId}-${uniqueId()}`;
@ -122,7 +133,7 @@ const Thread = ({
>
<RoundedHeaderContext/>
{Boolean(rootPost) &&
<>
<ExtraKeyboardProvider>
<View style={styles.flex}>
<ThreadPostList
nativeID={rootId}
@ -131,15 +142,12 @@ const Thread = ({
</View>
<PostDraft
channelId={rootPost!.channelId}
scrollViewNativeID={rootId}
accessoriesContainerID={THREAD_ACCESSORIES_CONTAINER_NATIVE_ID}
rootId={rootId}
keyboardTracker={postDraftRef}
testID='thread.post_draft'
containerHeight={containerHeight}
isChannelScreen={false}
/>
</>
</ExtraKeyboardProvider>
}
{showFloatingCallContainer &&
<FloatingCallContainer

View file

@ -3,7 +3,6 @@
import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import {ActivityIndicator, StyleSheet, View} from 'react-native';
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
import {fetchPostThread} from '@actions/remote/post';
import {markThreadAsRead} from '@actions/remote/thread';
@ -13,7 +12,7 @@ import {Screens} from '@constants';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {debounce} from '@helpers/api/general';
import {useAppState, useIsTablet} from '@hooks/device';
import {useAppState} from '@hooks/device';
import {useFetchingThreadState} from '@hooks/fetching_thread';
import {isMinimumServerVersion} from '@utils/helpers';
@ -31,8 +30,6 @@ type Props = {
version?: string;
}
const edges: Edge[] = ['bottom'];
const styles = StyleSheet.create({
container: {marginTop: 10},
flex: {flex: 1},
@ -44,7 +41,6 @@ const ThreadPostList = ({
nativeID, posts, rootPost, teamId, thread, version,
}: Props) => {
const appState = useAppState();
const isTablet = useIsTablet();
const serverUrl = useServerUrl();
const theme = useTheme();
const isFetchingThread = useFetchingThreadState(rootPost.id);
@ -87,7 +83,7 @@ const ThreadPostList = ({
oldPostsCount.current = posts.length;
markThreadAsRead(serverUrl, teamId, rootPost.id, false);
}
}, [isCRTEnabled, posts, rootPost, serverUrl, teamId, thread, appState === 'active']);
}, [isCRTEnabled, posts, rootPost, serverUrl, teamId, thread, appState]);
const lastViewedAt = isCRTEnabled ? (thread?.viewedAt ?? 0) : channelLastViewedAt;
@ -116,18 +112,7 @@ const ThreadPostList = ({
/>
);
if (isTablet) {
return postList;
}
return (
<SafeAreaView
edges={edges}
style={styles.flex}
>
{postList}
</SafeAreaView>
);
return postList;
};
export default ThreadPostList;

View file

@ -1,6 +1,8 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BehaviorSubject} from 'rxjs';
import type {AvailableScreens} from '@typings/screens/navigation';
class NavigationStore {
@ -9,11 +11,18 @@ class NavigationStore {
private visibleTab = 'Home';
private tosOpen = false;
private subject: BehaviorSubject<AvailableScreens|undefined> = new BehaviorSubject(undefined);
getSubject = () => {
return this.subject;
};
reset = () => {
this.screensInStack = [];
this.modalsInStack = [];
this.visibleTab = 'Home';
this.tosOpen = false;
this.subject.next(undefined);
};
addModalToStack = (modalId: AvailableScreens) => {
@ -25,10 +34,12 @@ class NavigationStore {
addScreenToStack = (screenId: AvailableScreens) => {
this.removeScreenFromStack(screenId);
this.screensInStack.unshift(screenId);
this.subject.next(screenId);
};
clearScreensFromStack = () => {
this.screensInStack = [];
this.subject.next(undefined);
};
getModalsInStack = () => this.modalsInStack;
@ -49,6 +60,7 @@ class NavigationStore {
const index = this.screensInStack.indexOf(screenId);
if (index > -1) {
this.screensInStack.splice(0, index);
this.subject.next(screenId);
}
};
@ -56,6 +68,7 @@ class NavigationStore {
const index = this.screensInStack.indexOf(screenId);
if (index > -1) {
this.screensInStack.splice(index, 1);
this.subject.next(this.screensInStack[0]);
}
};

View file

@ -94,27 +94,6 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- mattermost-keyboard-tracker (0.0.0):
- DoubleConversion
- glog
- hermes-engine
- RCT-Folly (= 2024.01.01.00)
- RCTRequired
- RCTTypeSafety
- React-Codegen
- React-Core
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-NativeModulesApple
- React-RCTFabric
- React-rendererdebug
- React-utils
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- mattermost-react-native-turbo-log (0.4.0):
- CocoaLumberjack (~> 3.8.5)
- DoubleConversion
@ -1669,7 +1648,6 @@ DEPENDENCIES:
- glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
- hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
- "mattermost-hardware-keyboard (from `../node_modules/@mattermost/hardware-keyboard`)"
- "mattermost-keyboard-tracker (from `../node_modules/@mattermost/keyboard-tracker`)"
- "mattermost-react-native-turbo-log (from `../node_modules/@mattermost/react-native-turbo-log`)"
- "mattermost-rnutils (from `../node_modules/@mattermost/rnutils`)"
- RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
@ -1812,8 +1790,6 @@ EXTERNAL SOURCES:
:tag: hermes-2024-06-28-RNv0.74.3-7bda0c267e76d11b68a585f84cfdd65000babf85
mattermost-hardware-keyboard:
:path: "../node_modules/@mattermost/hardware-keyboard"
mattermost-keyboard-tracker:
:path: "../node_modules/@mattermost/keyboard-tracker"
mattermost-react-native-turbo-log:
:path: "../node_modules/@mattermost/react-native-turbo-log"
mattermost-rnutils:
@ -2008,7 +1984,6 @@ SPEC CHECKSUMS:
JitsiWebRTC: d0ae5fd6a81e771bfd82c2ee6c6bb542ebd65ee8
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
mattermost-hardware-keyboard: b916a168bb3add779bb226b3a62d6ac5ec225a79
mattermost-keyboard-tracker: b48752a97e731fdae5e8259a4ea0e731e1a05094
mattermost-react-native-turbo-log: c446c7e8a39c131d6bafa6ae6f830c735030ed5e
mattermost-rnutils: 01504fb26beb75594f4c6e2150668b27783112f5
RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47

View file

@ -1,6 +0,0 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface UIResponder (CurrentResponder)
+(id)currentFirstResponder;
@end

View file

@ -1,17 +0,0 @@
#import "UIResponder+FirstResponder.h"
static __weak id currentFirstResponder;
@implementation UIResponder (FirstResponder)
+(id)currentFirstResponder {
currentFirstResponder = nil;
[[UIApplication sharedApplication] sendAction:@selector(findFirstResponder:) to:nil from:nil forEvent:nil];
return currentFirstResponder;
}
-(void)findFirstResponder:(id)sender {
currentFirstResponder = self;
}
@end

View file

@ -1,17 +0,0 @@
// This guard prevent this file to be compiled in the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
#import <React/RCTViewComponentView.h>
#import <UIKit/UIKit.h>
#ifndef MattermostKeyboardTrackerViewNativeComponent_h
#define MattermostKeyboardTrackerViewNativeComponent_h
NS_ASSUME_NONNULL_BEGIN
@interface MattermostKeyboardTrackerView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END
#endif /* MattermostKeyboardTrackerViewNativeComponent_h */
#endif /* RCT_NEW_ARCH_ENABLED */

View file

@ -1,169 +0,0 @@
#ifdef RCT_NEW_ARCH_ENABLED
#import "MattermostKeyboardTrackerView.h"
#import "KeyboardTrackingView.h"
#import <react/renderer/components/MattermostKeyboardTrackerSpec/ComponentDescriptors.h>
#import <react/renderer/components/MattermostKeyboardTrackerSpec/EventEmitters.h>
#import <react/renderer/components/MattermostKeyboardTrackerSpec/Props.h>
#import <react/renderer/components/MattermostKeyboardTrackerSpec/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
#import "RCTConversions.h"
using namespace facebook::react;
@interface MattermostKeyboardTrackerView () <RCTMattermostKeyboardTrackerViewViewProtocol>
@end
@implementation MattermostKeyboardTrackerView {
KeyboardTrackingView * _view;
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<MattermostKeyboardTrackerViewComponentDescriptor>();
}
-(std::shared_ptr<const MattermostKeyboardTrackerViewEventEmitter>)getEventEmitter {
if (!self->_eventEmitter) {
return nullptr;
}
assert(std::dynamic_pointer_cast<MattermostKeyboardTrackerViewEventEmitter const>(self->_eventEmitter));
return std::dynamic_pointer_cast<MattermostKeyboardTrackerViewEventEmitter const>(self->_eventEmitter);
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const MattermostKeyboardTrackerViewProps>();
_props = defaultProps;
_view = [[KeyboardTrackingView alloc] init];
[self _setOnKeyboardWillShow];
self.contentView = _view;
}
return self;
}
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
const auto &oldViewProps = *std::static_pointer_cast<MattermostKeyboardTrackerViewProps const>(_props);
const auto &newViewProps = *std::static_pointer_cast<MattermostKeyboardTrackerViewProps const>(props);
if (oldViewProps.scrollBehavior != newViewProps.scrollBehavior) {
std::string behaviorString = toString(newViewProps.scrollBehavior);
KeyboardTrackingScrollBehavior scrollBehavior = KeyboardTrackingScrollBehaviorFromString([NSString stringWithUTF8String:behaviorString.c_str()]);
_view.scrollBehavior = scrollBehavior;
}
if (oldViewProps.revealKeyboardInteractive != newViewProps.revealKeyboardInteractive) {
_view.revealKeyboardInteractive = newViewProps.revealKeyboardInteractive;
}
if (oldViewProps.manageScrollView != newViewProps.manageScrollView) {
_view.manageScrollView = newViewProps.manageScrollView;
}
if (oldViewProps.requiresSameParentToManageScrollView != newViewProps.requiresSameParentToManageScrollView) {
_view.requiresSameParentToManageScrollView = newViewProps.requiresSameParentToManageScrollView;
}
if (oldViewProps.addBottomView != newViewProps.addBottomView) {
_view.addBottomView = newViewProps.addBottomView;
}
if (oldViewProps.scrollToFocusedInput != newViewProps.scrollToFocusedInput) {
_view.scrollToFocusedInput = newViewProps.scrollToFocusedInput;
}
if (oldViewProps.allowHitsOutsideBounds != newViewProps.allowHitsOutsideBounds) {
_view.allowHitsOutsideBounds = newViewProps.allowHitsOutsideBounds;
}
if (oldViewProps.normalList != newViewProps.normalList) {
_view.normalList = newViewProps.normalList;
}
if (oldViewProps.viewInitialOffsetY != newViewProps.viewInitialOffsetY) {
_view.viewInitialOffsetY = newViewProps.viewInitialOffsetY;
}
if (oldViewProps.scrollViewNativeID != newViewProps.scrollViewNativeID) {
_view.scrollViewNativeID = RCTNSStringFromString(newViewProps.scrollViewNativeID);
}
if (oldViewProps.accessoriesContainerID != newViewProps.accessoriesContainerID) {
_view.accessoriesContainerID = RCTNSStringFromString(newViewProps.accessoriesContainerID);
}
if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
[_view setBottomViewBackgroundColor:RCTUIColorFromSharedColor(newViewProps.backgroundColor)];
}
[super updateProps:props oldProps:oldProps];
}
- (void)handleCommand:(nonnull const NSString *)commandName args:(nonnull const NSArray *)args {
RCTMattermostKeyboardTrackerViewHandleCommand(self, commandName, args);
}
-(void)_setOnKeyboardWillShow{
_view.onKeyboardWillShow = ^(NSDictionary *body) {
const auto eventEmitter = [self getEventEmitter];
if (eventEmitter) {
eventEmitter->onKeyboardWillShow(MattermostKeyboardTrackerViewEventEmitter::OnKeyboardWillShow{
.trackingViewHeight = [body[@"trackingViewHeight"] doubleValue],
.keyboardHeight = [body[@"keyboardHeight"] doubleValue],
.contentTopInset = [body[@"contentTopInset"] doubleValue],
.animationDuration = [body[@"animationDuration"] doubleValue],
.keyboardFrameEndHeight = (CGFloat)[body[@"keyboardFrameEndHeight"] doubleValue],
});
}
};
}
Class<RCTComponentViewProtocol> MattermostKeyboardTrackerViewCls(void)
{
return MattermostKeyboardTrackerView.class;
}
- (void)pauseTracking:(nonnull NSString *)scrollViewNativeID {
[_view pauseTracking:scrollViewNativeID];
}
- (void)resetScrollView:(nonnull NSString *)scrollViewNativeID {
[_view resetScrollView:scrollViewNativeID];
}
- (void)resumeTracking:(nonnull NSString *)scrollViewNativeID {
[_view resumeTracking:scrollViewNativeID];
}
- (void)scrollToStart {
[_view scrollToStart];
}
#pragma utils
KeyboardTrackingScrollBehavior KeyboardTrackingScrollBehaviorFromString(NSString *string) {
if ([string isEqualToString:@"KeyboardTrackingScrollBehaviorNone"]) {
return KeyboardTrackingScrollBehaviorNone;
} else if ([string isEqualToString:@"KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly"]) {
return KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly;
} else if ([string isEqualToString:@"KeyboardTrackingScrollBehaviorFixedOffset"]) {
return KeyboardTrackingScrollBehaviorFixedOffset;
} else {
// Handle the case where the string doesn't match any known value
// You can choose to return a default value or raise an exception
return KeyboardTrackingScrollBehaviorNone; // Default value
}
}
@end
#endif

View file

@ -1,127 +0,0 @@
#import <React/RCTViewManager.h>
#import <React/RCTUIManager.h>
#import "RCTBridge.h"
#import "KeyboardTrackingView.h"
@interface MattermostKeyboardTrackerViewManager : RCTViewManager
@property (nonatomic, nullable, strong) RCTDirectEventBlock keyboardWillShow;
@end
@implementation RCTConvert (KeyboardTrackingScrollBehavior)
RCT_ENUM_CONVERTER(KeyboardTrackingScrollBehavior, (@{ @"KeyboardTrackingScrollBehaviorNone": @(KeyboardTrackingScrollBehaviorNone),
@"KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly": @(KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly),
@"KeyboardTrackingScrollBehaviorFixedOffset": @(KeyboardTrackingScrollBehaviorFixedOffset)}),
KeyboardTrackingScrollBehaviorNone, unsignedIntegerValue)
@end
@implementation MattermostKeyboardTrackerViewManager
RCT_EXPORT_MODULE(MattermostKeyboardTrackerView)
#ifndef RCT_NEW_ARCH_ENABLED
- (UIView *)view
{
return [[KeyboardTrackingView alloc] init];
}
#endif
RCT_CUSTOM_VIEW_PROPERTY(scrollBehavior, NSString, KeyboardTrackingView) {
KeyboardTrackingScrollBehavior behavior = [RCTConvert KeyboardTrackingScrollBehavior:json];
[view setScrollBehavior:behavior];
}
RCT_CUSTOM_VIEW_PROPERTY(revealKeyboardInteractive, BOOL, KeyboardTrackingView) {
[view setRevealKeyboardInteractive:json];
}
RCT_CUSTOM_VIEW_PROPERTY(manageScrollView, BOOL, KeyboardTrackingView) {
[view setManageScrollView:json];
}
RCT_CUSTOM_VIEW_PROPERTY(requiresSameParentToManageScrollView, BOOL, KeyboardTrackingView) {
[view setRequiresSameParentToManageScrollView:json];
}
RCT_CUSTOM_VIEW_PROPERTY(addBottomView, BOOL, KeyboardTrackingView) {
[view setAddBottomView:json];
}
RCT_CUSTOM_VIEW_PROPERTY(scrollToFocusedInput, BOOL, KeyboardTrackingView) {
[view setScrollToFocusedInput:json];
}
RCT_CUSTOM_VIEW_PROPERTY(allowHitsOutsideBounds, BOOL, KeyboardTrackingView) {
[view setAllowHitsOutsideBounds:json];
}
RCT_CUSTOM_VIEW_PROPERTY(normalList, BOOL, KeyboardTrackingView) {
[view setNormalList:json];
}
RCT_CUSTOM_VIEW_PROPERTY(viewInitialOffsetY, CGFloat, KeyboardTrackingView) {
[view setViewInitialOffsetY:[RCTConvert CGFloat: json]];
}
RCT_CUSTOM_VIEW_PROPERTY(scrollViewNativeID, NSString, KeyboardTrackingView) {
[view setScrollViewNativeID:json];
}
RCT_CUSTOM_VIEW_PROPERTY(accessoriesContainerID, NSString, KeyboardTrackingView) {
[view setAccessoriesContainerID:json];
}
RCT_CUSTOM_VIEW_PROPERTY(backgroundColor, NSString, KeyboardTrackingView)
{
[view setBottomViewBackgroundColor:[RCTConvert UIColor:json]];
}
RCT_EXPORT_VIEW_PROPERTY(onKeyboardWillShow, RCTBubblingEventBlock)
#pragma Commands
RCT_EXPORT_METHOD(resetScrollView:(nonnull NSNumber *)reactTag scrollViewNativeID:(NSString *) scrollViewNativeID) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
KeyboardTrackingView *view = (KeyboardTrackingView *)viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[KeyboardTrackingView class]]) {
RCTLogError(@"Error: cannot find KeyboardTrackingView with tag #%@", reactTag);
return;
}
[view resetScrollView:scrollViewNativeID];
}];
}
RCT_EXPORT_METHOD(pauseTracking:(nonnull NSNumber *)reactTag scrollViewNativeID:(NSString*)scrollViewNativeID) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
KeyboardTrackingView *view = (KeyboardTrackingView *)viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[KeyboardTrackingView class]]) {
RCTLogError(@"Error: cannot find KeyboardTrackingView with tag #%@", reactTag);
return;
}
[view pauseTracking:scrollViewNativeID];
}];
}
RCT_EXPORT_METHOD(resumeTracking:(nonnull NSNumber *)reactTag scrollViewNativeID:(NSString*)scrollViewNativeID) {
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
KeyboardTrackingView *view = (KeyboardTrackingView *)viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[KeyboardTrackingView class]]) {
RCTLogError(@"Error: cannot find KeyboardTrackingView with tag #%@", reactTag);
return;
}
[view resumeTracking:scrollViewNativeID];
}];
}
RCT_EXPORT_METHOD(scrollToStart:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
KeyboardTrackingView *view = (KeyboardTrackingView *)viewRegistry[reactTag];
if (!view || ![view isKindOfClass:[KeyboardTrackingView class]]) {
RCTLogError(@"Error: cannot find KeyboardTrackingView with tag #%@", reactTag);
return;
}
[view scrollToStart];
}];
}
@end

View file

@ -1,50 +0,0 @@
#import <Foundation/Foundation.h>
#import <React/RCTComponent.h>
#import "ObservingInputAccessoryView.h"
typedef NS_ENUM(NSUInteger, KeyboardTrackingScrollBehavior) {
KeyboardTrackingScrollBehaviorNone,
KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly,
KeyboardTrackingScrollBehaviorFixedOffset
};
@interface KeyboardTrackingView : UIView
{
NSMapTable *_inputViewsMap;
ObservingInputAccessoryView *_observingInputAccessoryView;
UIView *_bottomView;
CGFloat _bottomViewHeight;
}
@property (nonatomic) NSMutableDictionary* _Nullable rctScrollViewsArray;
@property (nonatomic, strong) UIScrollView * _Nullable scrollViewToManage;
@property (nonatomic) BOOL scrollIsInverted;
@property (nonatomic) BOOL revealKeyboardInteractive;
@property (nonatomic) BOOL isDraggingScrollView;
@property (nonatomic) BOOL manageScrollView;
@property (nonatomic) BOOL requiresSameParentToManageScrollView;
@property (nonatomic) NSUInteger deferedInitializeAccessoryViewsCount;
@property (nonatomic) CGFloat originalHeight;
@property (nonatomic) KeyboardTrackingScrollBehavior scrollBehavior;
@property (nonatomic) BOOL addBottomView;
@property (nonatomic) BOOL scrollToFocusedInput;
@property (nonatomic) BOOL allowHitsOutsideBounds;
@property (nonatomic) BOOL normalList;
@property (nonatomic) NSString* _Nullable scrollViewNativeID;
@property (nonatomic) CGFloat initialOffsetY;
@property (nonatomic) CGFloat viewInitialOffsetY;
@property (nonatomic) BOOL initialOffsetIsSet;
@property (nonatomic) BOOL paused;
@property (nonatomic, strong) UIView* _Nullable accessoriesContainer;
@property (nonatomic) NSString* _Nullable accessoriesContainerID;
@property (nonatomic, copy, nullable) RCTDirectEventBlock onKeyboardWillShow;
-(void)setBottomViewBackgroundColor:(UIColor* _Nullable) color;
-(void)resetScrollView:(NSString* _Nullable) scrollViewNativeID;
-(void)pauseTracking:(NSString* _Nullable) scrollViewNativeID;
-(void)resumeTracking:(NSString* _Nullable) scrollViewNativeID;
-(void)scrollToStart;
@end

View file

@ -1,622 +0,0 @@
#import "KeyboardTrackingView.h"
#import <React/RCTUIManager.h>
#import <React/RCTScrollView.h>
#import "UIResponder+FirstResponder.h"
#import "ObservingInputAccessoryView.h"
#import <objc/runtime.h>
NSUInteger const kInputViewKey = 101010;
NSUInteger const kMaxDeferedInitializeAccessoryViews = 15;
NSInteger const kTrackingViewNotFoundErrorCode = 1;
NSInteger const kBottomViewHeight = 34;
@interface KeyboardTrackingView () <ObservingInputAccessoryViewDelegate, UIScrollViewDelegate>
@end
@implementation KeyboardTrackingView
-(instancetype)init
{
self = [super init];
if (self)
{
[self addObserver:self forKeyPath:@"bounds" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];
_inputViewsMap = [NSMapTable weakToWeakObjectsMapTable];
_deferedInitializeAccessoryViewsCount = 0;
_rctScrollViewsArray = [[NSMutableDictionary alloc] init];
_observingInputAccessoryView = [ObservingInputAccessoryView new];
_observingInputAccessoryView.delegate = self;
_initialOffsetY = 0;
_initialOffsetIsSet = NO;
_viewInitialOffsetY = 0;
_manageScrollView = YES;
_allowHitsOutsideBounds = NO;
_requiresSameParentToManageScrollView = YES;
_bottomViewHeight = kBottomViewHeight;
self.addBottomView = NO;
self.scrollToFocusedInput = NO;
self.paused = NO;
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(rctContentDidAppearNotification:) name:RCTContentDidAppearNotification object:nil];
}
return self;
}
-(RCTRootView*)getRootView
{
UIView *view = self;
while (view.superview != nil)
{
view = view.superview;
if ([view isKindOfClass:[RCTRootView class]])
break;
}
if ([view isKindOfClass:[RCTRootView class]])
{
return (RCTRootView*)view;
}
return nil;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (!_allowHitsOutsideBounds) {
return [super hitTest:point withEvent:event];
}
if (self.isHidden || self.alpha == 0 || self.clipsToBounds) {
return nil;
}
UIView *subview = [super hitTest:point withEvent:event];
if (subview == nil) {
NSArray<UIView*>* allSubviews = [self getBreadthFirstSubviewsForView:self];
for (UIView *tmpSubview in allSubviews) {
CGPoint pointInSubview = [self convertPoint:point toView:tmpSubview];
if ([tmpSubview pointInside:pointInSubview withEvent:event]) {
subview = tmpSubview;
break;
}
}
}
return subview;
}
-(void)layoutSubviews
{
[super layoutSubviews];
[self updateBottomViewFrame];
}
- (void)initializeAccessoryViewsAndHandleInsets
{
NSArray<UIView*>* allSubviews = [self getBreadthFirstSubviewsForView:[self getRootView]];
for (UIView* subview in allSubviews)
{
if(subview.nativeID) {
NSLog(@"self.accessoriesContainerID %@ %@", self.accessoriesContainerID, subview.nativeID);
}
if (subview.nativeID && [subview.nativeID isEqualToString:self.accessoriesContainerID]) {
NSLog(@"SuperView ID: %@", subview.nativeID);
_accessoriesContainer = subview;
}
if(_manageScrollView)
{
if(_scrollViewToManage == nil)
{
if([subview isKindOfClass:[RCTScrollView class]])
{
RCTScrollView *scrollView = (RCTScrollView*)subview;
if (subview.nativeID && [subview.nativeID isEqualToString:self.scrollViewNativeID]) {
[_rctScrollViewsArray setObject:scrollView forKey:subview.nativeID];
_scrollViewToManage = scrollView.scrollView;
}
}
}
}
if ([subview isKindOfClass:NSClassFromString(@"RCTTextField")])
{
UITextField *textField = nil;
Ivar backedTextInputIvar = class_getInstanceVariable([subview class], "_backedTextInput");
if (backedTextInputIvar != NULL)
{
textField = [subview valueForKey:@"_backedTextInput"];
}
else if([subview isKindOfClass:[UITextField class]])
{
textField = (UITextField*)subview;
}
[self setupTextField:textField];
}
else if ([subview isKindOfClass:NSClassFromString(@"RCTUITextField")] && [subview isKindOfClass:[UITextField class]])
{
[self setupTextField:(UITextField*)subview];
}
else if ([subview isKindOfClass:NSClassFromString(@"RCTMultilineTextInputView")])
{
[self setupTextView:[subview valueForKey:@"_backedTextInputView"]];
}
else if ([subview isKindOfClass:NSClassFromString(@"RCTTextView")])
{
UITextView *textView = nil;
Ivar backedTextInputIvar = class_getInstanceVariable([subview class], "_backedTextInput");
if (backedTextInputIvar != NULL)
{
textView = [subview valueForKey:@"_backedTextInput"];
}
else if([subview isKindOfClass:[UITextView class]])
{
textView = (UITextView*)subview;
}
[self setupTextView:textView];
}
else if ([subview isKindOfClass:NSClassFromString(@"RCTUITextView")] && [subview isKindOfClass:[UITextView class]])
{
[self setupTextView:(UITextView*)subview];
}
}
for (RCTScrollView *scrollView in [_rctScrollViewsArray allValues])
{
if(scrollView.scrollView == _scrollViewToManage)
{
[scrollView removeScrollListener:self];
[scrollView addScrollListener:self];
break;
}
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_10_3
if (@available(iOS 11.0, *)) {
if (_scrollViewToManage != nil) {
_scrollViewToManage.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}
}
#endif
[self _updateScrollViewInsets];
_originalHeight = _observingInputAccessoryView.height;
[self addBottomViewIfNecessary];
}
- (void)resetScrollView:(NSString*) scrollViewNativeID {
for (RCTScrollView *scrollView in [_rctScrollViewsArray allValues])
{
[scrollView removeScrollListener:self];
}
[_rctScrollViewsArray removeAllObjects];
_scrollViewToManage = nil;
_scrollViewNativeID = scrollViewNativeID;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self deferedInitializeAccessoryViewsAndHandleInsets];
[self scrollToStart];
});
}
-(void)pauseTracking:(NSString*) scrollViewNativeID {
if ([self.scrollViewNativeID isEqualToString:scrollViewNativeID]) {
_observingInputAccessoryView.delegate = nil;
self.scrollViewToManage = nil;
self.accessoriesContainer = nil;
self.paused = YES;
}
}
-(void)resumeTracking:(NSString*) scrollViewNativeID {
if ([self.scrollViewNativeID isEqualToString:scrollViewNativeID]) {
self.paused = NO;
_observingInputAccessoryView.delegate = self;
[self deferedInitializeAccessoryViewsAndHandleInsets];
}
}
- (void)setupTextView:(UITextView*)textView
{
if (textView != nil)
{
[textView setInputAccessoryView:_observingInputAccessoryView];
[textView reloadInputViews];
[_inputViewsMap setObject:textView forKey:@(kInputViewKey)];
}
}
- (void)setupTextField:(UITextField*)textField
{
if (textField != nil)
{
[textField setInputAccessoryView:_observingInputAccessoryView];
[textField reloadInputViews];
[_inputViewsMap setObject:textField forKey:@(kInputViewKey)];
}
}
-(void) deferedInitializeAccessoryViewsAndHandleInsets
{
if(self.window == nil)
{
return;
}
if (_observingInputAccessoryView.height == 0 && self.deferedInitializeAccessoryViewsCount < kMaxDeferedInitializeAccessoryViews)
{
self.deferedInitializeAccessoryViewsCount++;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self deferedInitializeAccessoryViewsAndHandleInsets];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
[self initializeAccessoryViewsAndHandleInsets];
});
}
}
- (void)willMoveToWindow:(nullable UIWindow *)newWindow
{
if (newWindow == nil && [ObservingInputAccessoryViewManager sharedInstance].activeObservingInputAccessoryView == _observingInputAccessoryView)
{
[ObservingInputAccessoryViewManager sharedInstance].activeObservingInputAccessoryView = nil;
}
else if (newWindow != nil)
{
[ObservingInputAccessoryViewManager sharedInstance].activeObservingInputAccessoryView = _observingInputAccessoryView;
}
}
-(void)didMoveToWindow
{
[super didMoveToWindow];
self.deferedInitializeAccessoryViewsCount = 0;
[self deferedInitializeAccessoryViewsAndHandleInsets];
}
-(void)dealloc
{
[self removeObserver:self forKeyPath:@"bounds"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context
{
if (self.paused) {
return;
}
_observingInputAccessoryView.height = self.bounds.size.height;
}
- (void)observingInputAccessoryViewKeyboardWillDisappear:(ObservingInputAccessoryView *)observingInputAccessoryView
{
if (self.paused) {
return;
}
_bottomViewHeight = [self getBottomSafeArea];
[self updateBottomViewFrame];
}
- (NSArray*)getBreadthFirstSubviewsForView:(UIView*)view
{
if(view == nil)
{
return nil;
}
NSMutableArray *allSubviews = [NSMutableArray new];
NSMutableArray *queue = [NSMutableArray new];
[allSubviews addObject:view];
[queue addObject:view];
while ([queue count] > 0) {
UIView *current = [queue lastObject];
[queue removeLastObject];
for (UIView *n in current.subviews)
{
[allSubviews addObject:n];
[queue insertObject:n atIndex:0];
}
}
return allSubviews;
}
- (NSArray*)getAllReactSubviewsForView:(UIView*)view
{
NSMutableArray *allSubviews = [NSMutableArray new];
for (UIView *subview in view.reactSubviews)
{
[allSubviews addObject:subview];
[allSubviews addObjectsFromArray:[self getAllReactSubviewsForView:subview]];
}
return allSubviews;
}
- (void)_updateScrollViewInsets
{
if(self.scrollViewToManage != nil && !self.paused)
{
if (!_initialOffsetIsSet) {
self.initialOffsetY = self.scrollViewToManage.contentOffset.y;
_initialOffsetIsSet = YES;
}
if (_observingInputAccessoryView.keyboardState != KeyboardStateWillHide && _observingInputAccessoryView.keyboardState != KeyboardStateHidden) {
[self.scrollViewToManage setContentOffset:CGPointMake(self.scrollViewToManage.contentOffset.x, self.initialOffsetY) animated:NO];
}
UIEdgeInsets insets = self.scrollViewToManage.contentInset;
CGFloat bottomSafeArea = [self getBottomSafeArea];
CGFloat bottomInset = MAX(self.bounds.size.height, _observingInputAccessoryView.keyboardHeight + _observingInputAccessoryView.height);
CGFloat originalBottomInset = self.scrollIsInverted ? insets.top : insets.bottom;
CGPoint originalOffset = self.scrollViewToManage.contentOffset;
CGFloat keyboardHeight = _observingInputAccessoryView.keyboardHeight;
bottomInset += (keyboardHeight == 0 ? bottomSafeArea : 0);
if(self.scrollIsInverted)
{
insets.top = bottomInset;
}
else
{
insets.bottom = keyboardHeight;
}
self.scrollViewToManage.contentInset = insets;
if(self.scrollBehavior == KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly && _scrollIsInverted)
{
BOOL firstTime = _observingInputAccessoryView.keyboardHeight == 0 && _observingInputAccessoryView.keyboardState == KeyboardStateHidden;
BOOL willOpen = _observingInputAccessoryView.keyboardHeight != 0 && _observingInputAccessoryView.keyboardState == KeyboardStateHidden;
BOOL isOpen = _observingInputAccessoryView.keyboardHeight != 0 && _observingInputAccessoryView.keyboardState == KeyboardStateShown;
if(firstTime || willOpen || (isOpen && !self.isDraggingScrollView))
{
[self.scrollViewToManage setContentOffset:CGPointMake(self.scrollViewToManage.contentOffset.x, self.scrollViewToManage.contentOffset.y) animated:!firstTime];
}
}
else if(self.scrollBehavior == KeyboardTrackingScrollBehaviorFixedOffset && !self.isDraggingScrollView)
{
CGFloat insetsDiff = (bottomInset - originalBottomInset) * (self.scrollIsInverted ? -1 : 1);
self.scrollViewToManage.contentOffset = CGPointMake(originalOffset.x, originalOffset.y + insetsDiff);
}
CGFloat kHeight = _observingInputAccessoryView.keyboardHeight;
if (kHeight != 0 && (_observingInputAccessoryView.keyboardState == KeyboardStateShown || _observingInputAccessoryView.keyboardState == KeyboardStateWillShow)) {
kHeight -= (bottomSafeArea + _viewInitialOffsetY);
}
CGFloat positionY = self.normalList ? 0 : kHeight;
CGRect frame = CGRectMake(self.scrollViewToManage.frame.origin.x, positionY,
self.scrollViewToManage.frame.size.width, self.scrollViewToManage.frame.size.height);
self.scrollViewToManage.frame = frame;
if (self.accessoriesContainer) {
CGFloat containerPositionY = self.normalList ? 0 : kHeight;
self.accessoriesContainer.bounds = CGRectMake(self.accessoriesContainer.bounds.origin.x, containerPositionY,
self.accessoriesContainer.bounds.size.width, self.accessoriesContainer.bounds.size.height);
}
}
}
#pragma mark - bottom view
-(void)setAddBottomView:(BOOL)addBottomView
{
_addBottomView = addBottomView;
[self addBottomViewIfNecessary];
}
-(void)addBottomViewIfNecessary
{
if (self.addBottomView && _bottomView == nil)
{
_bottomView = [UIView new];
[self addSubview:_bottomView];
[self updateBottomViewFrame];
}
else if (!self.addBottomView && _bottomView != nil)
{
[_bottomView removeFromSuperview];
_bottomView = nil;
}
}
-(void)updateBottomViewFrame
{
if (_bottomView != nil)
{
_bottomView.frame = CGRectMake(0, self.frame.size.height, self.frame.size.width, _bottomViewHeight);
}
}
-(void)setBottomViewBackgroundColor:(UIColor*) color {
if (_bottomView != nil) {
_bottomView.backgroundColor = color;
}
}
#pragma mark - safe area
-(void)safeAreaInsetsDidChange
{
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_10_3
if (@available(iOS 11.0, *)) {
[super safeAreaInsetsDidChange];
}
#endif
[self updateTransformAndInsets];
}
-(CGFloat)getBottomSafeArea
{
CGFloat bottomSafeArea = 0;
#if __IPHONE_OS_VERSION_MAX_ALLOWED > __IPHONE_10_3
if (@available(iOS 11.0, *)) {
bottomSafeArea = self.superview ? self.superview.safeAreaInsets.bottom : self.safeAreaInsets.bottom;
}
#endif
return bottomSafeArea;
}
#pragma RCTRootView notifications
- (void) rctContentDidAppearNotification:(NSNotification*)notification
{
dispatch_async(dispatch_get_main_queue(), ^{
if(notification.object == [self getRootView] && self->_manageScrollView && self->_scrollViewToManage == nil)
{
[self initializeAccessoryViewsAndHandleInsets];
}
});
}
#pragma mark - ObservingInputAccessoryViewDelegate methods
-(void)updateTransformAndInsets
{
if (self.paused) {
return;
}
CGFloat bottomSafeArea = [self getBottomSafeArea];
CGFloat accessoryTranslation = MIN(-bottomSafeArea, -(_observingInputAccessoryView.keyboardHeight - _viewInitialOffsetY));
if (_observingInputAccessoryView.keyboardHeight <= bottomSafeArea) {
_bottomViewHeight = [self getBottomSafeArea];
} else if (_observingInputAccessoryView.keyboardState != KeyboardStateWillHide) {
_bottomViewHeight = 0;
}
[self updateBottomViewFrame];
self.transform = CGAffineTransformMakeTranslation(0, accessoryTranslation);
[self _updateScrollViewInsets];
}
- (void)performScrollToFocusedInput
{
if (_scrollViewToManage != nil && self.scrollToFocusedInput)
{
UIResponder *currentFirstResponder = [UIResponder currentFirstResponder];
if (currentFirstResponder != nil && [currentFirstResponder isKindOfClass:[UIView class]])
{
UIView *reponderView = (UIView*)currentFirstResponder;
if ([reponderView isDescendantOfView:_scrollViewToManage])
{
CGRect frame = [_scrollViewToManage convertRect:reponderView.frame fromView:reponderView];
frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width, frame.size.height + 20);
[_scrollViewToManage scrollRectToVisible:frame animated:NO];
}
}
}
}
- (void)observingInputAccessoryViewDidChangeFrame:(ObservingInputAccessoryView*)observingInputAccessoryView
{
[self updateTransformAndInsets];
}
-(void) observingInputAccessoryViewKeyboardWillAppear:(ObservingInputAccessoryView *)observingInputAccessoryView keyboardDelta:(CGFloat)keyboardDelta animationDuration:(double)animationDuration keyboardFrameEndHeight:(CGFloat)keyboardFrameEndHeight {
if (observingInputAccessoryView.keyboardHeight > 0) //prevent hiding the bottom view if an external keyboard is in use
{
_bottomViewHeight = 0;
[self updateBottomViewFrame];
}
[self performScrollToFocusedInput];
_onKeyboardWillShow(@{
@"trackingViewHeight": [NSNumber numberWithDouble:self.bounds.size.height],
@"keyboardHeight": [NSNumber numberWithDouble:[self getKeyboardHeight]],
@"contentTopInset": [NSNumber numberWithDouble:[self getScrollViewTopContentInset]],
@"animationDuration": [NSNumber numberWithDouble:animationDuration * 1000],
@"keyboardFrameEndHeight": [NSNumber numberWithDouble:keyboardFrameEndHeight],
});
}
#pragma mark - UIScrollViewDelegate methods
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(_observingInputAccessoryView.keyboardState != KeyboardStateHidden || !self.revealKeyboardInteractive)
{
return;
}
UIView *inputView = [_inputViewsMap objectForKey:@(kInputViewKey)];
if (inputView != nil && scrollView.contentOffset.y * (self.scrollIsInverted ? -1 : 1) > (self.scrollIsInverted ? scrollView.contentInset.top : scrollView.contentInset.bottom) + 50 && ![inputView isFirstResponder])
{
for (UIGestureRecognizer *gesture in scrollView.gestureRecognizers)
{
if([gesture isKindOfClass:[UIPanGestureRecognizer class]])
{
gesture.enabled = NO;
gesture.enabled = YES;
}
}
[inputView reactFocus];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
self.isDraggingScrollView = YES;
_initialOffsetIsSet = NO;
self.initialOffsetY = scrollView.contentOffset.y;
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
self.isDraggingScrollView = NO;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
self.isDraggingScrollView = NO;
self.initialOffsetY = scrollView.contentOffset.y;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
self.initialOffsetY = scrollView.contentOffset.y;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.initialOffsetY = scrollView.contentOffset.y;
}
- (CGFloat)getKeyboardHeight
{
return _observingInputAccessoryView ? _observingInputAccessoryView.keyboardHeight : 0;
}
-(CGFloat)getScrollViewTopContentInset
{
return (self.scrollViewToManage != nil) ? -self.scrollViewToManage.contentInset.top : 0;
}
-(void)scrollToStart
{
if (self.scrollViewToManage != nil)
{
[self.scrollViewToManage setContentOffset:CGPointMake(self.scrollViewToManage.contentOffset.x, -self.scrollViewToManage.contentInset.top) animated:YES];
}
}
@end

View file

@ -1,36 +0,0 @@
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, KeyboardState) {
KeyboardStateHidden,
KeyboardStateWillShow,
KeyboardStateShown,
KeyboardStateWillHide
};
@class ObservingInputAccessoryView;
@interface ObservingInputAccessoryViewManager : NSObject;
+(ObservingInputAccessoryViewManager*)sharedInstance;
@property (nonatomic, weak) ObservingInputAccessoryView *activeObservingInputAccessoryView;
@end
@protocol ObservingInputAccessoryViewDelegate <NSObject>
- (void)observingInputAccessoryViewDidChangeFrame:(ObservingInputAccessoryView*)observingInputAccessoryView;
@optional
- (void) observingInputAccessoryViewKeyboardWillAppear:(ObservingInputAccessoryView *)observingInputAccessoryView keyboardDelta:(CGFloat)keyboardDelta animationDuration:(double)animationDuration keyboardFrameEndHeight:(CGFloat)keyboardFrameEndHeight;
- (void)observingInputAccessoryViewKeyboardWillDisappear:(ObservingInputAccessoryView*)observingInputAccessoryView;
@end
@interface ObservingInputAccessoryView : UIView
@property (nonatomic, weak) id<ObservingInputAccessoryViewDelegate> delegate;
@property (nonatomic) CGFloat height;
@property (nonatomic, readonly) CGFloat keyboardHeight;
@property (nonatomic, readonly) KeyboardState keyboardState;
@end

View file

@ -1,172 +0,0 @@
#import "ObservingInputAccessoryView.h"
@implementation ObservingInputAccessoryViewManager
+(ObservingInputAccessoryViewManager*)sharedInstance
{
static ObservingInputAccessoryViewManager *instance = nil;
static dispatch_once_t observingInputAccessoryViewManagerOnceToken = 0;
dispatch_once(&observingInputAccessoryViewManagerOnceToken,^
{
if (instance == nil)
{
instance = [ObservingInputAccessoryViewManager new];
}
});
return instance;
}
@end
@implementation ObservingInputAccessoryView
{
CGFloat _previousKeyboardHeight;
}
- (instancetype)init
{
self = [super init];
if(self)
{
self.userInteractionEnabled = NO;
self.translatesAutoresizingMaskIntoConstraints = NO;
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
[self registerForKeyboardNotifications];
}
return self;
}
- (void) registerForKeyboardNotifications
{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:@selector(_keyboardWillShowNotification:) name:UIKeyboardWillShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(_keyboardDidShowNotification:) name:UIKeyboardDidShowNotification object:nil];
[notificationCenter addObserver:self selector:@selector(_keyboardWillHideNotification:) name:UIKeyboardWillHideNotification object:nil];
[notificationCenter addObserver:self selector:@selector(_keyboardDidHideNotification:) name:UIKeyboardDidHideNotification object:nil];
[notificationCenter addObserver:self selector:@selector(_keyboardWillChangeFrameNotification:) name:UIKeyboardWillChangeFrameNotification object:nil];
}
- (void)willMoveToSuperview:(UIView *)newSuperview
{
if (self.superview)
{
[self.superview removeObserver:self forKeyPath:@"center"];
}
if (newSuperview != nil)
{
[newSuperview addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionNew context:nil];
}
[super willMoveToSuperview:newSuperview];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ((object == self.superview) && ([keyPath isEqualToString:@"center"]))
{
CGFloat centerY = self.superview.center.y;
if([keyPath isEqualToString:@"center"])
{
centerY = [change[NSKeyValueChangeNewKey] CGPointValue].y;
}
CGFloat boundsH = self.superview.bounds.size.height;
_previousKeyboardHeight = _keyboardHeight;
_keyboardHeight = MAX(0, self.window.bounds.size.height - (centerY - boundsH / 2) - self.intrinsicContentSize.height);
[_delegate observingInputAccessoryViewDidChangeFrame:self];
}
}
-(void)dealloc
{
[self.superview removeObserver:self forKeyPath:@"center"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (CGSize)intrinsicContentSize
{
return CGSizeMake(self.bounds.size.width, _keyboardState == KeyboardStateWillShow || _keyboardState == KeyboardStateWillHide ? 0 : _height);
}
- (void)setHeight:(CGFloat)height
{
_height = height;
[self invalidateIntrinsicContentSize];
}
- (void)_keyboardWillShowNotification:(NSNotification*)notification
{
if (_keyboardState != KeyboardStateShown) {
_keyboardState = KeyboardStateWillShow;
[self invalidateIntrinsicContentSize];
if([_delegate respondsToSelector:@selector(observingInputAccessoryViewKeyboardWillAppear:keyboardDelta:animationDuration:keyboardFrameEndHeight:)])
{
NSDictionary *userInfo = notification.userInfo;
CGFloat delta = _keyboardHeight - _previousKeyboardHeight;
NSValue *frameEndValue = userInfo[UIKeyboardFrameEndUserInfoKey];
CGFloat keyboardFrameEnd = [frameEndValue CGRectValue].size.height;
NSNumber *animationDurationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey];
NSTimeInterval animationDuration = [animationDurationValue doubleValue];
[_delegate observingInputAccessoryViewKeyboardWillAppear:self keyboardDelta:delta animationDuration:animationDuration keyboardFrameEndHeight:keyboardFrameEnd];
}
}
}
- (void)_keyboardDidShowNotification:(NSNotification*)notification
{
if (_keyboardState != KeyboardStateShown) {
_keyboardState = KeyboardStateShown;
[self invalidateIntrinsicContentSize];
}
}
- (void)_keyboardWillHideNotification:(NSNotification*)notification
{
_keyboardState = KeyboardStateWillHide;
[self invalidateIntrinsicContentSize];
if([_delegate respondsToSelector:@selector(observingInputAccessoryViewKeyboardWillDisappear:)])
{
[_delegate observingInputAccessoryViewKeyboardWillDisappear:self];
}
}
- (void)_keyboardDidHideNotification:(NSNotification*)notification
{
_keyboardState = KeyboardStateHidden;
[self invalidateIntrinsicContentSize];
}
- (void)_keyboardWillChangeFrameNotification:(NSNotification*)notification
{
if(self.window)
{
return;
}
CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
_keyboardHeight = [UIScreen mainScreen].bounds.size.height - endFrame.origin.y;
[_delegate observingInputAccessoryViewDidChangeFrame:self];
[self invalidateIntrinsicContentSize];
}
@end

View file

@ -1,23 +0,0 @@
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
Pod::Spec.new do |s|
s.name = "mattermost-keyboard-tracker"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm}"
install_modules_dependencies(s)
s.pod_target_xcconfig = {
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
}
end

View file

@ -1,16 +0,0 @@
{
"name": "@mattermost/keyboard-tracker",
"version": "0.0.0",
"description": "component to track the keyboard",
"main": "src/index",
"codegenConfig": {
"name": "MattermostKeyboardTrackerSpec",
"type": "all",
"jsSrcsDir": "src",
"platforms": ["ios"]
},
"author": "Mattermost, Inc.",
"license": "Apache 2.0",
"private": true,
"homepage": "#readme"
}

View file

@ -1,70 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import codegenNativeCommands from 'react-native/Libraries/Utilities/codegenNativeCommands';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type {ColorValue, HostComponent, ViewProps} from 'react-native';
import type {BubblingEventHandler, Double, Float, WithDefault} from 'react-native/Libraries/Types/CodegenTypes';
type ScrollBehavior = 'KeyboardTrackingScrollBehaviorNone' | 'KeyboardTrackingScrollBehaviorScrollToBottomInvertedOnly' | 'KeyboardTrackingScrollBehaviorFixedOffset'
export type NativeKeyboardTrackerProps = Readonly<{
trackingViewHeight: Double;
keyboardHeight: Double;
contentTopInset: Double;
animationDuration: Double;
keyboardFrameEndHeight: Float;
}>
export type KeyboardWillShowEventData = {
nativeEvent: {
trackingViewHeight: number;
keyboardHeight: number;
contentTopInset: number;
animationDuration: number;
keyboardFrameEndHeight: number;
};
}
export interface NativeProps extends ViewProps {
scrollBehavior?: WithDefault<ScrollBehavior, 'KeyboardTrackingScrollBehaviorNone'>;
revealKeyboardInteractive?: boolean;
manageScrollView?: WithDefault<boolean, true>;
requiresSameParentToManageScrollView?: WithDefault<boolean, true>;
addBottomView?: boolean;
scrollToFocusedInput?: boolean;
allowHitsOutsideBounds?: boolean;
normalList?: boolean;
viewInitialOffsetY?: WithDefault<Float, 0>;
scrollViewNativeID?: string;
accessoriesContainerID?: string;
backgroundColor?: ColorValue;
onKeyboardWillShow?: BubblingEventHandler<NativeKeyboardTrackerProps>;
}
export type MattermostKeyboardTrackerNativeComponentType = HostComponent<NativeProps>;
interface NativeCommands {
readonly resetScrollView: (
viewRef: React.ElementRef<MattermostKeyboardTrackerNativeComponentType>,
scrollViewNativeID: string,
) => void;
readonly pauseTracking: (
viewRef: React.ElementRef<MattermostKeyboardTrackerNativeComponentType>,
scrollViewNativeID: string,
) => void;
readonly resumeTracking: (
viewRef: React.ElementRef<MattermostKeyboardTrackerNativeComponentType>,
scrollViewNativeID: string,
) => void;
readonly scrollToStart: (
viewRef: React.ElementRef<MattermostKeyboardTrackerNativeComponentType>,
) => void;
}
export const Commands: NativeCommands = codegenNativeCommands<NativeCommands>({
supportedCommands: ['resetScrollView', 'pauseTracking', 'resumeTracking', 'scrollToStart'],
});
export default codegenNativeComponent<NativeProps>('MattermostKeyboardTrackerView');

View file

@ -1,64 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {forwardRef, useCallback, useImperativeHandle, useRef} from 'react';
import {DeviceEventEmitter} from 'react-native';
import MattermostKeyboardTrackerView, {Commands, type KeyboardWillShowEventData, type NativeProps} from './MattermostKeyboardTrackerViewNativeComponent';
export * from './MattermostKeyboardTrackerViewNativeComponent';
export type KeyboardTrackingViewRef = {
resetScrollView: (scrollViewNativeID: string) => void;
pauseTracking: (scrollViewNativeID: string) => void;
resumeTracking: (scrollViewNativeID: string) => void;
scrollToStart: () => void;
};
type Props = NativeProps & {
onKeyboardWillShow?: (e: KeyboardWillShowEventData) => void;
}
const KeyboardTrackingView = forwardRef<KeyboardTrackingViewRef, Props>((props, ref) => {
const viewRef = useRef(null);
useImperativeHandle(ref, () => ({
resetScrollView(scrollViewNativeID: string) {
if (viewRef.current) {
Commands.resumeTracking(viewRef.current, scrollViewNativeID);
}
},
pauseTracking(scrollViewNativeID: string) {
if (viewRef.current) {
Commands.pauseTracking(viewRef.current, scrollViewNativeID);
}
},
resumeTracking(scrollViewNativeID: string) {
if (viewRef.current) {
Commands.resumeTracking(viewRef.current, scrollViewNativeID);
}
},
scrollToStart() {
if (viewRef.current) {
Commands.scrollToStart(viewRef.current);
}
},
}));
const _onKeyboardShow = useCallback((e: KeyboardWillShowEventData) => {
DeviceEventEmitter.emit('MattermostKeyboardTrackerView', e);
props.onKeyboardWillShow?.(e);
}, [props.onKeyboardWillShow]);
return (
<MattermostKeyboardTrackerView
{...props}
onKeyboardWillShow={_onKeyboardShow}
ref={viewRef}
/>
);
});
KeyboardTrackingView.displayName = 'KeyboardTrackingView';
export default KeyboardTrackingView;

View file

@ -2,6 +2,8 @@ package com.mattermost.rnutils
import android.app.Activity
import android.net.Uri
import android.view.WindowManager
import androidx.core.view.OnApplyWindowInsetsListener
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
@ -13,6 +15,8 @@ import com.mattermost.rnutils.helpers.SaveDataTask
import com.mattermost.rnutils.helpers.SplitView
class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
private var customInsetsListener: OnApplyWindowInsetsListener? = null
companion object {
const val NAME = "RNUtils"
@ -121,4 +125,20 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
fun removeServerNotifications(serverUrl: String?) {
serverUrl?.let { Notifications.removeServerNotifications(it) }
}
fun setSoftKeyboardToAdjustNothing() {
val currentActivity: Activity = reactContext.currentActivity ?: return
currentActivity.runOnUiThread {
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING)
}
}
fun setSoftKeyboardToAdjustResize() {
val currentActivity: Activity = reactContext.currentActivity ?: return
currentActivity.runOnUiThread {
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
}
}

View file

@ -67,4 +67,12 @@ class RNUtilsModule(val reactContext: ReactApplicationContext) : NativeRNUtilsSp
override fun removeServerNotifications(serverUrl: String?) {
implementation.removeServerNotifications(serverUrl)
}
override fun setSoftKeyboardToAdjustResize() {
implementation.setSoftKeyboardToAdjustResize()
}
override fun setSoftKeyboardToAdjustNothing() {
implementation.setSoftKeyboardToAdjustNothing()
}
}

View file

@ -98,4 +98,14 @@ class RNUtilsModule(context: ReactApplicationContext) :
fun removeServerNotifications(serverUrl: String?) {
implementation.removeServerNotifications(serverUrl)
}
@ReactMethod
fun setSoftKeyboardToAdjustResize() {
implementation.setSoftKeyboardToAdjustResize()
}
@ReactMethod
fun setSoftKeyboardToAdjustNothing() {
implementation.setSoftKeyboardToAdjustNothing()
}
}

View file

@ -123,6 +123,14 @@ RCT_EXPORT_METHOD(saveFile:(NSString *)filePath
[self saveFile:filePath resolve:resolve reject:reject];
}
RCT_REMAP_METHOD(setSoftKeyboardToAdjustResize, setAdjustResize) {
[self setSoftKeyboardToAdjustResize];
}
RCT_REMAP_METHOD(setSoftKeyboardToAdjustNothing, setAdjustNothing) {
[self setSoftKeyboardToAdjustNothing];
}
// Don't compile this code when we build for the old architecture.
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<TurboModule>)getTurboModule:
@ -216,6 +224,14 @@ RCT_EXPORT_METHOD(saveFile:(NSString *)filePath
resolve(@"");
}
-(void)setSoftKeyboardToAdjustResize {
// Do nothing as it does not apply to iOS
}
-(void)setSoftKeyboardToAdjustNothing {
// Do nothing as it does not apply to iOS
}
#pragma helpers
-(void)getNotifications:(RCTPromiseResolveBlock)resolve {
[[NotificationManager shared] getDeliveredNotificationsWithCompletionHandler:^(NSArray<NSDictionary *> * _Nonnull notifications) {

View file

@ -70,6 +70,9 @@ export interface Spec extends TurboModule {
removeChannelNotifications(serverUrl: string, channelId: string): void;
removeThreadNotifications(serverUrl: string, threadId: string): void;
removeServerNotifications(serverUrl: string): void;
setSoftKeyboardToAdjustResize(): void;
setSoftKeyboardToAdjustNothing(): void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('RNUtils');

8
package-lock.json generated
View file

@ -6,7 +6,7 @@
"packages": {
"": {
"name": "mattermost-mobile",
"version": "2.22.0",
"version": "2.23.0",
"hasInstallScript": true,
"license": "Apache 2.0",
"dependencies": {
@ -20,7 +20,6 @@
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
"@mattermost/compass-icons": "0.1.45",
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.7.3",
"@mattermost/react-native-paste-input": "0.8.0",
@ -176,6 +175,7 @@
},
"libraries/@mattermost/keyboard-tracker": {
"version": "0.0.0",
"extraneous": true,
"license": "Apache 2.0"
},
"libraries/@mattermost/rnshare": {
@ -5880,10 +5880,6 @@
"resolved": "libraries/@mattermost/hardware-keyboard",
"link": true
},
"node_modules/@mattermost/keyboard-tracker": {
"resolved": "libraries/@mattermost/keyboard-tracker",
"link": true
},
"node_modules/@mattermost/react-native-emm": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.5.0.tgz",

View file

@ -21,7 +21,6 @@
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
"@mattermost/compass-icons": "0.1.45",
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
"@mattermost/keyboard-tracker": "file:./libraries/@mattermost/keyboard-tracker",
"@mattermost/react-native-emm": "1.5.0",
"@mattermost/react-native-network-client": "1.7.3",
"@mattermost/react-native-paste-input": "0.8.0",

View file

@ -296,6 +296,37 @@ index 834d734..3fc00c3 100644
want |= Typeface.ITALIC;
}
diff --git a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/child/ChildController.java b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/child/ChildController.java
index 3acc41d..ebc8d18 100644
--- a/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/child/ChildController.java
+++ b/node_modules/react-native-navigation/lib/android/app/src/main/java/com/reactnativenavigation/viewcontrollers/child/ChildController.java
@@ -7,6 +7,7 @@ import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
+import android.view.WindowManager;
import com.reactnativenavigation.options.Options;
import com.reactnativenavigation.utils.LogKt;
@@ -100,6 +101,18 @@ public abstract class ChildController<T extends ViewGroup> extends ViewControlle
}
protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
+ Activity activity = getActivity();
+ Insets ime = insets.getInsets(WindowInsetsCompat.Type.ime());
+ if (activity != null) {
+ int softInputMode = activity.getWindow().getAttributes().softInputMode;
+ if (softInputMode == WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING && !isRoot()) {
+ view.setPadding(ime.left, ime.top, ime.right, 0);
+ return WindowInsetsCompat.CONSUMED;
+ }
+ }
+ if (!isRoot()) {
+ view.setPadding(0, 0, 0, 0);
+ }
return insets;
}
diff --git a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java b/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java
index 035ec31..630b8d4 100644
--- a/node_modules/react-native-navigation/lib/android/app/src/reactNative71/java/com/reactnativenavigation/react/ReactGateway.java

View file

@ -7,6 +7,7 @@ import React, {type ReactElement} from 'react';
import {IntlProvider} from 'react-intl';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
import ServerUrlProvider from '@context/server';
import {ThemeContext, getDefaultThemeByAppearance} from '@context/theme';
import {getTranslations} from '@i18n';
@ -63,7 +64,9 @@ export function renderWithEverything(ui: ReactElement, {locale = 'en', database,
>
<ThemeContext.Provider value={getDefaultThemeByAppearance()}>
<SafeAreaProvider>
{children}
<ExtraKeyboardProvider>
{children}
</ExtraKeyboardProvider>
</SafeAreaProvider>
</ThemeContext.Provider>
</IntlProvider>