diff --git a/app/components/channel_item/channel_item.tsx b/app/components/channel_item/channel_item.tsx index 1627ce8b0..3037efe61 100644 --- a/app/components/channel_item/channel_item.tsx +++ b/app/components/channel_item/channel_item.tsx @@ -142,17 +142,17 @@ const ChannelListItem = ({ styles.text, isBright && styles.highlight, isMuted && styles.muted, - isActive && !isInfo ? styles.textActive : null, + isActive && isTablet && !isInfo ? styles.textActive : null, isInfo ? styles.textInfo : null, - ], [isBright, styles, isMuted, isActive, isInfo]); + ], [isBright, styles, isMuted, isActive, isInfo, isTablet]); const containerStyle = useMemo(() => [ styles.container, - isActive && !isInfo && styles.activeItem, + isActive && isTablet && !isInfo && styles.activeItem, isInfo && styles.infoItem, {minHeight: height}, ], - [height, isActive, isInfo, styles]); + [height, isActive, isTablet, isInfo, styles]); if (!hasMember) { return null; @@ -176,7 +176,7 @@ const ChannelListItem = ({ 0} diff --git a/app/components/floating_text_input_label/index.tsx b/app/components/floating_text_input_label/index.tsx index 81788ed1c..f38baac55 100644 --- a/app/components/floating_text_input_label/index.tsx +++ b/app/components/floating_text_input_label/index.tsx @@ -224,8 +224,8 @@ const FloatingTextInput = forwardRef diff --git a/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap b/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap index 95b0a60fe..bf82088ed 100644 --- a/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap +++ b/app/components/server_icon/__snapshots__/server_icon.test.tsx.snap @@ -11,6 +11,14 @@ exports[`Server Icon Server Icon Component should match snapshot 1`] = ` accessible={true} collapsable={false} focusable={false} + hitSlop={ + Object { + "bottom": 5, + "left": 40, + "right": 20, + "top": 20, + } + } onClick={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} @@ -44,6 +52,14 @@ exports[`Server Icon Server Icon Component should match snapshot with mentions 1 accessible={true} collapsable={false} focusable={false} + hitSlop={ + Object { + "bottom": 5, + "left": 40, + "right": 20, + "top": 20, + } + } onClick={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} @@ -112,6 +128,14 @@ exports[`Server Icon Server Icon Component should match snapshot with unreads 1` accessible={true} collapsable={false} focusable={false} + hitSlop={ + Object { + "bottom": 5, + "left": 40, + "right": 20, + "top": 20, + } + } onClick={[Function]} onResponderGrant={[Function]} onResponderMove={[Function]} diff --git a/app/components/server_icon/index.tsx b/app/components/server_icon/index.tsx index a730ab341..327c9daad 100644 --- a/app/components/server_icon/index.tsx +++ b/app/components/server_icon/index.tsx @@ -25,6 +25,8 @@ type Props = { unreadStyle?: StyleProp; } +const hitSlop = {top: 20, bottom: 5, left: 40, right: 20}; + const styles = StyleSheet.create({ badge: { left: 13, @@ -64,6 +66,7 @@ export default function ServerIcon({ onPress={onPress} type='opacity' testID={testID} + hitSlop={hitSlop} > { }, mentionsOneDigit: { top: 1, - left: 28, + left: 31, }, mentionsTwoDigits: { top: 1, - left: 26, + left: 30, }, mentionsThreeDigits: { top: 1, - left: 23, + left: 28, }, }; }); diff --git a/app/constants/events.ts b/app/constants/events.ts index f27945df6..9994db2d7 100644 --- a/app/constants/events.ts +++ b/app/constants/events.ts @@ -8,6 +8,7 @@ export default keyMirror({ CHANNEL_DELETED: null, CLOSE_BOTTOM_SHEET: null, CONFIG_CHANGED: null, + FETCHING_POSTS: null, FREEZE_SCREEN: null, GALLERY_ACTIONS: null, LEAVE_CHANNEL: null, diff --git a/app/screens/channel/channel.tsx b/app/screens/channel/channel.tsx index 595e1097e..dd52980a1 100644 --- a/app/screens/channel/channel.tsx +++ b/app/screens/channel/channel.tsx @@ -2,7 +2,7 @@ // See LICENSE.txt for license information. import React, {useEffect, useRef, useState} from 'react'; -import {DeviceEventEmitter, StyleSheet, View} from 'react-native'; +import {BackHandler, DeviceEventEmitter, NativeEventSubscription, StyleSheet, View} from 'react-native'; import {KeyboardTrackingViewRef} from 'react-native-keyboard-tracking-view'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; @@ -13,6 +13,7 @@ import {ACCESSORIES_CONTAINER_NATIVE_ID} from '@constants/post_draft'; import {useAppState, useIsTablet} from '@hooks/device'; import {useDefaultHeaderHeight} from '@hooks/header'; import {useTeamSwitch} from '@hooks/team_switch'; +import {popTopScreen} from '@screens/navigation'; import ChannelPostList from './channel_post_list'; import ChannelHeader from './header'; @@ -52,6 +53,18 @@ const Channel = ({channelId, componentId}: ChannelProps) => { return () => listener.remove(); }, []); + useEffect(() => { + let back: NativeEventSubscription|undefined; + if (!isTablet && componentId) { + back = BackHandler.addEventListener('hardwareBackPress', () => { + popTopScreen(componentId); + return true; + }); + } + + return () => back?.remove(); + }, [componentId, isTablet]); + const marginTop = defaultHeight + (isTablet ? insets.top : 0); useEffect(() => { // This is done so that the header renders diff --git a/app/screens/channel/channel_post_list/channel_post_list.tsx b/app/screens/channel/channel_post_list/channel_post_list.tsx index 3e55710a7..8b5a582a6 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.tsx +++ b/app/screens/channel/channel_post_list/channel_post_list.tsx @@ -54,7 +54,12 @@ const ChannelPostList = ({ } }, 500), [channelId, posts]); - const intro = ; + const intro = ( + 0} + /> + ); const postList = ( { - const [fetching, setFetching] = useState(false); +const Intro = ({channel, hasPosts, roles}: Props) => { + const [fetching, setFetching] = useState(!hasPosts); const theme = useTheme(); const element = useMemo(() => { if (channel.type === General.OPEN_CHANNEL && channel.name === General.DEFAULT_CHANNEL) { @@ -78,7 +78,23 @@ const Intro = ({channel, loading = false, roles}: Props) => { return () => listener.remove(); }, []); - if (loading || fetching) { + // We add a timeout to remove the loading indicator + // Even if the channel does not have any posts + useEffect(() => { + const time = setTimeout(() => { + if (!hasPosts && fetching) { + setFetching(false); + } + }, 1000); + + return () => { + if (time) { + clearTimeout(time); + } + }; + }, [hasPosts, fetching]); + + if (fetching) { return ( { - bounce.current = debounce(search, 250); + bounce.current = debounce(search, 500); bounce.current(); return () => { if (bounce.current) { diff --git a/app/screens/find_channels/index.tsx b/app/screens/find_channels/index.tsx index ad191b6e8..3b0ec8e50 100644 --- a/app/screens/find_channels/index.tsx +++ b/app/screens/find_channels/index.tsx @@ -1,7 +1,8 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {debounce, DebouncedFunc} from 'lodash'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {DeviceEventEmitter, Keyboard, View} from 'react-native'; import {Navigation} from 'react-native-navigation'; @@ -44,6 +45,7 @@ const FindChannels = ({closeButtonId, componentId}: Props) => { const theme = useTheme(); const [term, setTerm] = useState(''); const [loading, setLoading] = useState(false); + const bounce = useRef void>>(); const styles = getStyleSheet(theme); const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]); const keyboardHeight = useKeyboardHeight(); @@ -66,10 +68,18 @@ const FindChannels = ({closeButtonId, componentId}: Props) => { }, []); const onChangeText = useCallback((text) => { - setTerm(text); - if (!text) { + if (text) { + bounce.current?.cancel(); + bounce.current = debounce(() => { + setTerm(text); + }, 100); + bounce.current(); + } else { + setTerm(text); setLoading(false); } + + return () => bounce.current?.cancel(); }, []); useEffect(() => { diff --git a/app/screens/gallery/footer/footer.tsx b/app/screens/gallery/footer/footer.tsx index ebb3f3c1f..6dd501e7a 100644 --- a/app/screens/gallery/footer/footer.tsx +++ b/app/screens/gallery/footer/footer.tsx @@ -38,7 +38,7 @@ type Props = { } const AnimatedSafeAreaView = Animated.createAnimatedComponent(SafeAreaView); -const edges: Edge[] = ['left', 'right']; +const edges: Edge[] = ['left', 'right', 'bottom']; const styles = StyleSheet.create({ container: { alignItems: 'center', diff --git a/app/screens/home/channel_list/channel_list.tsx b/app/screens/home/channel_list/channel_list.tsx index 915116333..b5d2586e8 100644 --- a/app/screens/home/channel_list/channel_list.tsx +++ b/app/screens/home/channel_list/channel_list.tsx @@ -2,17 +2,20 @@ // See LICENSE.txt for license information. import {useManagedConfig} from '@mattermost/react-native-emm'; -import {useIsFocused, useRoute} from '@react-navigation/native'; -import React, {useEffect} from 'react'; -import {StyleSheet} from 'react-native'; +import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; +import React, {useCallback, useEffect} from 'react'; +import {useIntl} from 'react-intl'; +import {BackHandler, StyleSheet, ToastAndroid} from 'react-native'; import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import FreezeScreen from '@components/freeze_screen'; import TeamSidebar from '@components/team_sidebar'; +import {Screens} from '@constants'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {resetToTeams} from '@screens/navigation'; +import EphemeralStore from '@store/ephemeral_store'; import AdditionalTabletView from './additional_tablet_view'; import CategoriesList from './categories_list'; @@ -34,17 +37,43 @@ const styles = StyleSheet.create({ }, }); +let backPressedCount = 0; +let backPressTimeout: NodeJS.Timeout|undefined; + const ChannelListScreen = (props: ChannelProps) => { const theme = useTheme(); const managedConfig = useManagedConfig(); + const intl = useIntl(); const isTablet = useIsTablet(); const route = useRoute(); const isFocused = useIsFocused(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const params = route.params as {direction: string}; const canAddOtherServers = managedConfig?.allowOtherServers !== 'false'; + const handleBackPress = useCallback(() => { + const focused = navigation.isFocused() && EphemeralStore.getNavigationTopComponentId() === Screens.HOME; + if (!backPressedCount && focused) { + backPressedCount++; + ToastAndroid.show(intl.formatMessage({ + id: 'mobile.android.back_handler_exit', + defaultMessage: 'Press back again to exit', + }), ToastAndroid.SHORT); + + if (backPressTimeout) { + clearTimeout(backPressTimeout); + } + backPressTimeout = setTimeout(() => { + clearTimeout(backPressTimeout!); + backPressedCount = 0; + }, 2000); + return true; + } + return false; + }, [intl]); + const animated = useAnimatedStyle(() => { if (!isFocused) { let initial = 0; @@ -72,6 +101,11 @@ const ChannelListScreen = (props: ChannelProps) => { } }, [Boolean(props.teamsCount)]); + useEffect(() => { + const back = BackHandler.addEventListener('hardwareBackPress', handleBackPress); + return () => back.remove(); + }, [handleBackPress]); + return ( {} diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index 09103866e..d82c39cdd 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -197,9 +197,9 @@ const ServerItem = ({ viewRef.current?.measureInWindow((x, y, w, h) => { const bounds: TutorialItemBounds = { startX: x - 20, - startY: y - 5, + startY: y, endX: x + w + 20, - endY: y + h + 5, + endY: y + h, }; setShowTutorial(true); setItemBounds(bounds); @@ -343,7 +343,7 @@ const ServerItem = ({ useEffect(() => { let time: NodeJS.Timeout; if (highlight && !tutorialWatched) { - time = setTimeout(startTutorial, 300); + time = setTimeout(startTutorial, 650); } return () => clearTimeout(time); }, [highlight, tutorialWatched]); diff --git a/app/screens/home/index.tsx b/app/screens/home/index.tsx index b310d766b..e3aef911d 100644 --- a/app/screens/home/index.tsx +++ b/app/screens/home/index.tsx @@ -32,6 +32,7 @@ if (Platform.OS === 'ios') { enableFreeze(true); type HomeProps = LaunchProps & { + componentId: string; time?: number; }; diff --git a/app/screens/server/form.tsx b/app/screens/server/form.tsx index 1cb14ee9b..e22c2aed3 100644 --- a/app/screens/server/form.tsx +++ b/app/screens/server/form.tsx @@ -17,6 +17,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = { + autoFocus?: boolean; buttonDisabled: boolean; connecting: boolean; displayName?: string; @@ -71,6 +72,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); const ServerForm = ({ + autoFocus = false, buttonDisabled, connecting, displayName = '', @@ -163,6 +165,7 @@ const ServerForm = ({