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:
parent
732b17a75c
commit
d25c6fe245
41 changed files with 427 additions and 1658 deletions
|
|
@ -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/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
183
app/context/extra_keyboard/index.tsx
Normal file
183
app/context/extra_keyboard/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIResponder (CurrentResponder)
|
||||
+(id)currentFirstResponder;
|
||||
@end
|
||||
|
|
@ -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
|
||||
|
|
@ -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 */
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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');
|
||||
|
|
@ -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;
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
8
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue