From 62d2e20441e059cfcc7716a229dc8628ad306054 Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Wed, 1 Jun 2022 17:07:54 -0400 Subject: [PATCH] [Gekidou] Navigation bar refactored (#6319) * Navigation bar refactored * feedback review * add MAX_OVERSCROLL constant --- app/components/navigation_header/context.tsx | 55 --------- app/components/navigation_header/header.tsx | 11 +- app/components/navigation_header/index.tsx | 63 ++++------ app/components/navigation_header/large.tsx | 6 +- app/components/navigation_header/search.tsx | 47 ++++++-- .../navigation_header/search_context.tsx | 44 ------- .../rounded_header_context/index.tsx | 2 +- app/hooks/header.ts | 112 +++++++++--------- app/screens/channel/header/header.tsx | 36 ++++-- .../channel_info_form.tsx | 2 +- app/screens/global_threads/index.tsx | 8 ++ .../home/recent_mentions/recent_mentions.tsx | 54 +++++---- app/screens/home/search/index.tsx | 50 +++++--- app/screens/home/search/results/header.tsx | 8 +- .../home/search/results/header_button.tsx | 4 +- app/screens/home/search/results/results.tsx | 33 +++--- 16 files changed, 253 insertions(+), 282 deletions(-) delete mode 100644 app/components/navigation_header/context.tsx delete mode 100644 app/components/navigation_header/search_context.tsx diff --git a/app/components/navigation_header/context.tsx b/app/components/navigation_header/context.tsx deleted file mode 100644 index 41c856f774..0000000000 --- a/app/components/navigation_header/context.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import Animated, {useAnimatedStyle} from 'react-native-reanimated'; - -import RoundedHeaderContext from '@components/rounded_header_context'; -import {HEADER_SEARCH_BOTTOM_MARGIN, HEADER_SEARCH_HEIGHT} from '@constants/view'; - -type Props = { - defaultHeight: number; - hasSearch: boolean; - isLargeTitle: boolean; - largeHeight: number; - scrollValue?: Animated.SharedValue; - top: number; -} - -const NavigationHeaderContext = ({ - defaultHeight, - hasSearch, - isLargeTitle, - largeHeight, - scrollValue, - top, -}: Props) => { - const marginTop = useAnimatedStyle(() => { - const normal = defaultHeight + top; - const value = scrollValue?.value || 0; - let margin: number; - if (isLargeTitle) { - const searchHeight = hasSearch ? HEADER_SEARCH_HEIGHT + HEADER_SEARCH_BOTTOM_MARGIN : 0; - margin = Math.max((-value + largeHeight + searchHeight), normal); - } else { - const calculated = -(top + value); - margin = Math.max((normal + calculated), normal); - } - - return { - position: 'absolute', - width: '100%', - height: '100%', - marginTop: margin, - }; - }, [defaultHeight, largeHeight, isLargeTitle, hasSearch]); - - return ( - - - - ); -}; - -export default NavigationHeaderContext; - diff --git a/app/components/navigation_header/header.tsx b/app/components/navigation_header/header.tsx index 4db06d6d3d..7bf31a996c 100644 --- a/app/components/navigation_header/header.tsx +++ b/app/components/navigation_header/header.tsx @@ -151,10 +151,15 @@ const Header = ({ return {opacity: 0}; } - const barHeight = Platform.OS === 'ios' ? (largeHeight - defaultHeight - (top / 2)) : largeHeight - defaultHeight; - const val = top + (scrollValue?.value ?? 0); + const largeTitleLabelHeight = 60; + const barHeight = (largeHeight - defaultHeight) - largeTitleLabelHeight; + const val = (scrollValue?.value ?? 0); + const showDuration = 200; + const hideDuration = 50; + const duration = val >= barHeight ? showDuration : hideDuration; + const opacityValue = val >= barHeight ? 1 : 0; return { - opacity: val >= barHeight ? withTiming(1, {duration: 250}) : 0, + opacity: withTiming(opacityValue, {duration}), }; }, [defaultHeight, largeHeight, isLargeTitle, hasSearch]); diff --git a/app/components/navigation_header/index.tsx b/app/components/navigation_header/index.tsx index 5b5500c626..8fab83257e 100644 --- a/app/components/navigation_header/index.tsx +++ b/app/components/navigation_header/index.tsx @@ -6,14 +6,12 @@ import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {useTheme} from '@context/theme'; -import useHeaderHeight from '@hooks/header'; +import useHeaderHeight, {MAX_OVERSCROLL} from '@hooks/header'; import {makeStyleSheetFromTheme} from '@utils/theme'; -import NavigationHeaderContext from './context'; import Header, {HeaderRightButton} from './header'; import NavigationHeaderLargeTitle from './large'; import NavigationSearch from './search'; -import NavigationHeaderSearchContext from './search_context'; import type {SearchProps} from '@components/search'; @@ -25,9 +23,8 @@ type Props = SearchProps & { onTitlePress?: () => void; rightButtons?: HeaderRightButton[]; scrollValue?: Animated.SharedValue; - hideHeader?: (visible: boolean) => void; + hideHeader?: () => void; showBackButton?: boolean; - showHeaderInContext?: boolean; subtitle?: string; subtitleCompanion?: React.ReactElement; title?: string; @@ -51,7 +48,6 @@ const NavigationHeader = ({ rightButtons, scrollValue, showBackButton, - showHeaderInContext = true, subtitle, subtitleCompanion, title = '', @@ -62,12 +58,17 @@ const NavigationHeader = ({ const insets = useSafeAreaInsets(); const styles = getStyleSheet(theme); - const {largeHeight, defaultHeight} = useHeaderHeight(isLargeTitle, Boolean(subtitle), hasSearch); + const {largeHeight, defaultHeight} = useHeaderHeight(); const containerHeight = useAnimatedStyle(() => { - const normal = defaultHeight + insets.top; - const calculated = -(insets.top + (scrollValue?.value || 0)); - return {height: Math.max((normal + calculated), normal)}; - }, []); + const minHeight = defaultHeight + insets.top; + const value = -(scrollValue?.value || 0); + const height = ((isLargeTitle ? largeHeight : defaultHeight)) + value + insets.top; + return { + height: Math.max(height, minHeight), + minHeight, + maxHeight: largeHeight + insets.top + MAX_OVERSCROLL, + }; + }); return ( <> @@ -98,38 +99,20 @@ const NavigationHeader = ({ subtitle={subtitle} theme={theme} title={title} - top={insets.top} /> } + {hasSearch && + + } - {hasSearch && - <> - - - - } - {showHeaderInContext && - - } ); }; diff --git a/app/components/navigation_header/large.tsx b/app/components/navigation_header/large.tsx index 72cd2b7f00..51ddb0ac06 100644 --- a/app/components/navigation_header/large.tsx +++ b/app/components/navigation_header/large.tsx @@ -16,7 +16,6 @@ type Props = { subtitle?: string; theme: Theme; title: string; - top: number; } const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ @@ -42,16 +41,15 @@ const NavigationHeaderLargeTitle = ({ subtitle, theme, title, - top, }: Props) => { const styles = getStyleSheet(theme); const transform = useAnimatedStyle(() => { const value = scrollValue?.value || 0; return { - transform: [{translateY: -(top + value)}], + transform: [{translateY: Math.min(-value, largeHeight - defaultHeight)}], }; - }, [top]); + }); const containerStyle = useMemo(() => { return [{height: largeHeight - defaultHeight}, styles.container]; diff --git a/app/components/navigation_header/search.tsx b/app/components/navigation_header/search.tsx index ad56b962d8..34e56c5229 100644 --- a/app/components/navigation_header/search.tsx +++ b/app/components/navigation_header/search.tsx @@ -1,32 +1,36 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useMemo} from 'react'; -import {Platform} from 'react-native'; +import React, {useCallback, useEffect, useMemo} from 'react'; +import {DeviceEventEmitter, Keyboard, Platform} from 'react-native'; import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import Search, {SearchProps} from '@components/search'; +import {Events} from '@constants'; import {HEADER_SEARCH_HEIGHT} from '@constants/view'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; type Props = SearchProps & { + defaultHeight: number; largeHeight: number; scrollValue?: Animated.SharedValue; - hideHeader?: (visible: boolean) => void; + hideHeader?: () => void; theme: Theme; top: number; } +const INITIAL_TOP = -45; + const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { backgroundColor: theme.sidebarBg, height: HEADER_SEARCH_HEIGHT, - justifyContent: 'flex-start', + justifyContent: 'center', paddingHorizontal: 20, - position: 'absolute', width: '100%', zIndex: 10, + top: INITIAL_TOP, }, inputContainerStyle: { backgroundColor: changeOpacity(theme.sidebarText, 0.12), @@ -37,11 +41,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); const NavigationSearch = ({ + defaultHeight, largeHeight, scrollValue, - hideHeader: setHeaderVisibility, + hideHeader, theme, - top, ...searchProps }: Props) => { const styles = getStyleSheet(theme); @@ -55,13 +59,34 @@ const NavigationSearch = ({ }), [theme]); const searchTop = useAnimatedStyle(() => { - return {marginTop: Math.max((-(scrollValue?.value || 0) + largeHeight), top)}; - }, [largeHeight, top]); + const value = scrollValue?.value || 0; + const min = (largeHeight - defaultHeight); + return {marginTop: Math.min(-Math.min((value), min), min)}; + }, [largeHeight, defaultHeight]); const onFocus = useCallback((e) => { - setHeaderVisibility?.(false); + hideHeader?.(); searchProps.onFocus?.(e); - }, [setHeaderVisibility, searchProps.onFocus]); + }, [hideHeader, searchProps.onFocus]); + + useEffect(() => { + const show = Keyboard.addListener('keyboardDidShow', () => { + if (Platform.OS === 'android') { + DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, false); + } + }); + + const hide = Keyboard.addListener('keyboardDidHide', () => { + if (Platform.OS === 'android') { + DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true); + } + }); + + return () => { + hide.remove(); + show.remove(); + }; + }, []); return ( diff --git a/app/components/navigation_header/search_context.tsx b/app/components/navigation_header/search_context.tsx deleted file mode 100644 index 8d4b67eab8..0000000000 --- a/app/components/navigation_header/search_context.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; -import Animated, {useAnimatedStyle} from 'react-native-reanimated'; - -import {HEADER_SEARCH_BOTTOM_MARGIN, HEADER_SEARCH_HEIGHT} from '@constants/view'; -import {makeStyleSheetFromTheme} from '@utils/theme'; - -type Props = { - defaultHeight: number; - largeHeight: number; - scrollValue?: Animated.SharedValue; - theme: Theme; -} - -const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ - container: { - backgroundColor: theme.sidebarBg, - height: HEADER_SEARCH_BOTTOM_MARGIN * 2, - position: 'absolute', - width: '100%', - }, -})); - -const NavigationHeaderSearchContext = ({ - defaultHeight, - largeHeight, - scrollValue, - theme, -}: Props) => { - const styles = getStyleSheet(theme); - - const marginTop = useAnimatedStyle(() => { - return {marginTop: (largeHeight + HEADER_SEARCH_HEIGHT) - (scrollValue?.value || 0)}; - }, [defaultHeight, largeHeight]); - - return ( - - ); -}; - -export default NavigationHeaderSearchContext; - diff --git a/app/components/rounded_header_context/index.tsx b/app/components/rounded_header_context/index.tsx index 65b36c0eab..0e4d1cff57 100644 --- a/app/components/rounded_header_context/index.tsx +++ b/app/components/rounded_header_context/index.tsx @@ -10,7 +10,7 @@ import {makeStyleSheetFromTheme} from '@utils/theme'; const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ container: { backgroundColor: theme.sidebarBg, - height: '100%', + height: 40, width: '100%', position: 'absolute', }, diff --git a/app/hooks/header.ts b/app/hooks/header.ts index 1ff4a053b3..4c712101de 100644 --- a/app/hooks/header.ts +++ b/app/hooks/header.ts @@ -3,17 +3,20 @@ import React, {useCallback, useMemo} from 'react'; import {NativeScrollEvent, Platform} from 'react-native'; -import Animated, {scrollTo, useAnimatedRef, useAnimatedScrollHandler, useSharedValue} from 'react-native-reanimated'; +import Animated, {runOnJS, scrollTo, useAnimatedRef, useAnimatedScrollHandler, useDerivedValue, useSharedValue} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import ViewConstants, {HEADER_SEARCH_BOTTOM_MARGIN} from '@constants/view'; +import ViewConstants from '@constants/view'; import {useIsTablet} from '@hooks/device'; type HeaderScrollContext = { - momentum?: number; + dragging?: boolean; + momentum?: string; start?: number; }; +export const MAX_OVERSCROLL = 80; + export const useDefaultHeaderHeight = () => { const isTablet = useIsTablet(); @@ -28,93 +31,98 @@ export const useDefaultHeaderHeight = () => { return ViewConstants.ANDROID_DEFAULT_HEADER_HEIGHT; }; -export const useLargeHeaderHeight = (hasLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => { +export const useLargeHeaderHeight = () => { const defaultHeight = useDefaultHeaderHeight(); - if (hasLargeTitle && hasSubtitle && !hasSearch) { - return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SUBTITLE; - } else if (hasLargeTitle && hasSearch) { - return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SEARCH_HEIGHT; - } - - return defaultHeight + ViewConstants.LARGE_HEADER_TITLE; + return defaultHeight + ViewConstants.LARGE_HEADER_TITLE + ViewConstants.HEADER_WITH_SUBTITLE; }; -export const useHeaderHeight = (hasLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => { +export const useHeaderHeight = () => { const defaultHeight = useDefaultHeaderHeight(); - const largeHeight = useLargeHeaderHeight(hasLargeTitle, hasSubtitle, hasSearch); + const largeHeight = useLargeHeaderHeight(); return useMemo(() => { return { defaultHeight, largeHeight, }; - }, [defaultHeight, hasSearch, largeHeight]); + }, [defaultHeight, largeHeight]); }; -export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => { +export const useCollapsibleHeader = (isLargeTitle: boolean, onSnap?: (offset: number) => void) => { const insets = useSafeAreaInsets(); const animatedRef = useAnimatedRef(); - const {largeHeight, defaultHeight} = useHeaderHeight(true, hasSubtitle, hasSearch); + const {largeHeight, defaultHeight} = useHeaderHeight(); const scrollValue = useSharedValue(0); + const autoScroll = useSharedValue(false); + const snapping = useSharedValue(false); + + const headerHeight = useDerivedValue(() => { + const minHeight = defaultHeight + insets.top; + const value = -(scrollValue?.value || 0); + const header = (isLargeTitle ? largeHeight : defaultHeight); + const height = header + value + insets.top; + if (height > header + (insets.top * 2)) { + return Math.min(height, largeHeight + insets.top + MAX_OVERSCROLL); + } + return Math.max(height, minHeight); + }); function snapIfNeeded(dir: string, offset: number) { 'worklet'; - if (dir === 'up' && offset < defaultHeight) { - const diffHeight = largeHeight - defaultHeight; - let position = 0; - if (Platform.OS === 'ios') { - position = (diffHeight - (hasSearch ? -HEADER_SEARCH_BOTTOM_MARGIN : insets.top)); - } else { - position = hasSearch ? largeHeight + HEADER_SEARCH_BOTTOM_MARGIN : diffHeight; - } - scrollTo(animatedRef, 0, position, true); - } else if (dir === 'down') { - const inset = largeHeight + (hasSearch ? HEADER_SEARCH_BOTTOM_MARGIN : 0); - if (offset < inset) { - scrollTo(animatedRef, 0, -insets.top, true); + if (onSnap && !snapping.value) { + snapping.value = true; + if (dir === 'down' && offset < largeHeight) { + runOnJS(onSnap)(0); + } else if (dir === 'up' && offset < (defaultHeight + insets.top)) { + runOnJS(onSnap)((largeHeight - defaultHeight)); } + snapping.value = false; } } const onScroll = useAnimatedScrollHandler({ onBeginDrag: (e: NativeScrollEvent, ctx: HeaderScrollContext) => { ctx.start = e.contentOffset.y; + ctx.dragging = true; }, - onScroll: (e) => { - scrollValue.value = e.contentOffset.y; + onScroll: (e, ctx) => { + if (ctx.dragging || autoScroll.value) { + scrollValue.value = e.contentOffset.y; + } else { + // here we want to ensure that the scroll position + // always start at 0 if the user has not dragged + // the scrollview manually + scrollValue.value = 0; + scrollTo(animatedRef, 0, 0, false); + } }, onEndDrag: (e, ctx) => { - if (ctx.start !== undefined && Platform.OS === 'ios') { + if (ctx.start !== undefined) { const dir = e.contentOffset.y < ctx.start ? 'down' : 'up'; const offset = Math.abs(e.contentOffset.y); - ctx.start = undefined; snapIfNeeded(dir, offset); } }, onMomentumBegin: (e, ctx) => { - ctx.momentum = Platform.OS === 'ios' ? e.contentOffset.y : ctx.start; + ctx.momentum = e.contentOffset.y < (ctx.start || 0) ? 'down' : 'up'; }, onMomentumEnd: (e, ctx) => { - if (ctx.momentum !== undefined) { + ctx.start = undefined; + ctx.dragging = false; + if (ctx.momentum === 'down') { const offset = Math.abs(e.contentOffset.y); - const dir = e.contentOffset.y < ctx.momentum ? 'down' : 'up'; - ctx.momentum = undefined; - if (Platform.OS === 'android') { - // This avoids snapping to the defaultHeight when already at the top and scrolling down - if (dir === 'up' && offset === 0) { - return; - } - snapIfNeeded(dir, offset); - } else if (dir === 'down' && offset < (defaultHeight + (hasSearch ? HEADER_SEARCH_BOTTOM_MARGIN : 0))) { - scrollTo(animatedRef, 0, -insets.top, true); + if (onSnap && offset < largeHeight) { + runOnJS(onSnap)(0); } + ctx.momentum = undefined; } }, - }, [insets, defaultHeight, largeHeight]); + }, [insets, defaultHeight, largeHeight, animatedRef]); const hideHeader = useCallback(() => { - const offset = largeHeight + HEADER_SEARCH_BOTTOM_MARGIN; + const offset = largeHeight - defaultHeight; if (animatedRef?.current && Math.abs((scrollValue?.value || 0)) <= insets.top) { + autoScroll.value = true; if ('scrollTo' in animatedRef.current) { animatedRef.current.scrollTo({y: offset, animated: true}); } else if ('scrollToOffset' in animatedRef.current) { @@ -128,17 +136,15 @@ export const useCollapsibleHeader = (isLargeTitle: boolean, hasSubtitle: bool } }, [largeHeight, defaultHeight]); - let searchPadding = 0; - if (hasSearch) { - searchPadding = ViewConstants.HEADER_SEARCH_HEIGHT + ViewConstants.HEADER_SEARCH_BOTTOM_MARGIN; - } - return { - scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight) + searchPadding, + defaultHeight, + largeHeight, + scrollPaddingTop: (isLargeTitle ? largeHeight : defaultHeight) + insets.top, scrollRef: animatedRef as unknown as React.RefObject, scrollValue, onScroll, hideHeader, + headerHeight, }; }; diff --git a/app/screens/channel/header/header.tsx b/app/screens/channel/header/header.tsx index e97baf302f..88008aafd3 100644 --- a/app/screens/channel/header/header.tsx +++ b/app/screens/channel/header/header.tsx @@ -4,13 +4,16 @@ import React, {useCallback, useMemo} from 'react'; import {useIntl} from 'react-intl'; import {DeviceEventEmitter, Keyboard, Platform, Text, View} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; import CompassIcon from '@components/compass_icon'; import CustomStatusEmoji from '@components/custom_status/custom_status_emoji'; import NavigationHeader from '@components/navigation_header'; +import RoundedHeaderContext from '@components/rounded_header_context'; import {Navigation, Screens} from '@constants'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; +import {useDefaultHeaderHeight} from '@hooks/header'; import {bottomSheet, popTopScreen, showModal} from '@screens/navigation'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -63,6 +66,12 @@ const ChannelHeader = ({ const isTablet = useIsTablet(); const theme = useTheme(); const styles = getStyleSheet(theme); + const defaultHeight = useDefaultHeaderHeight(); + const insets = useSafeAreaInsets(); + + const contextStyle = useMemo(() => ({ + top: defaultHeight + insets.top, + }), [defaultHeight, insets.top]); const leftComponent = useMemo(() => { if (isTablet || !channelId || !teamId) { @@ -163,17 +172,22 @@ const ChannelHeader = ({ }, [memberCount, customStatus, isCustomStatusExpired]); return ( - + <> + + + + + ); }; diff --git a/app/screens/create_or_edit_channel/channel_info_form.tsx b/app/screens/create_or_edit_channel/channel_info_form.tsx index 17cec65bf7..aa61b004b6 100644 --- a/app/screens/create_or_edit_channel/channel_info_form.tsx +++ b/app/screens/create_or_edit_channel/channel_info_form.tsx @@ -109,7 +109,7 @@ export default function ChannelInfoForm({ const intl = useIntl(); const {formatMessage} = intl; const isTablet = useIsTablet(); - const headerHeight = useHeaderHeight(false, false, false); + const headerHeight = useHeaderHeight(); const theme = useTheme(); const styles = getStyleSheet(theme); diff --git a/app/screens/global_threads/index.tsx b/app/screens/global_threads/index.tsx index e052a63e98..33deb4e03c 100644 --- a/app/screens/global_threads/index.tsx +++ b/app/screens/global_threads/index.tsx @@ -7,6 +7,7 @@ import {Keyboard, StyleSheet, View} from 'react-native'; import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context'; import NavigationHeader from '@components/navigation_header'; +import RoundedHeaderContext from '@components/rounded_header_context'; import {useAppState, useIsTablet} from '@hooks/device'; import {useDefaultHeaderHeight} from '@hooks/header'; import {useTeamSwitch} from '@hooks/team_switch'; @@ -42,6 +43,10 @@ const GlobalThreads = ({componentId}: Props) => { return {flex: 1, marginTop}; }, [defaultHeight, insets.top]); + const contextStyle = useMemo(() => ({ + top: defaultHeight + insets.top, + }), [defaultHeight, insets.top]); + const onBackPress = useCallback(() => { Keyboard.dismiss(); popTopScreen(componentId); @@ -65,6 +70,9 @@ const GlobalThreads = ({componentId}: Props) => { }) } /> + + + {!switchingTeam && { + scrollRef.current?.scrollToOffset({offset, animated: true}); + }; + + useEffect(() => { + opacity.value = isFocused ? 1 : 0; + translateX.value = isFocused ? 0 : translateSide; + }, [isFocused]); useEffect(() => { setLoading(true); @@ -71,23 +80,11 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr }); }, [serverUrl]); - const {scrollPaddingTop, scrollRef, scrollValue, onScroll} = useCollapsibleHeader>(isLargeTitle, Boolean(subtitle), false); - - const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop}), [scrollPaddingTop]); - + const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight} = useCollapsibleHeader>(true, onSnap); + const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop - insets.top, flexGrow: 1}), [scrollPaddingTop, insets.top]); + const scrollViewStyle = useMemo(() => ({top: insets.top}), [insets.top]); const posts = useMemo(() => selectOrderedPosts(mentions, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [mentions]); - const handleRefresh = useCallback(async () => { - setRefreshing(true); - await fetchRecentMentions(serverUrl); - setRefreshing(false); - }, [serverUrl]); - - useEffect(() => { - opacity.value = isFocused ? 1 : 0; - translateX.value = isFocused ? 0 : translateSide; - }, [isFocused]); - const animated = useAnimatedStyle(() => { return { opacity: withTiming(opacity.value, {duration: 150}), @@ -95,6 +92,18 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr }; }, []); + const top = useAnimatedStyle(() => { + return { + top: headerHeight.value, + }; + }); + + const handleRefresh = useCallback(async () => { + setRefreshing(true); + await fetchRecentMentions(serverUrl); + setRefreshing(false); + }, [serverUrl]); + const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => { if (!viewableItems.length) { return; @@ -111,7 +120,7 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr }, []); const renderEmptyList = useCallback(() => ( - + {loading ? ( + + + diff --git a/app/screens/home/search/index.tsx b/app/screens/home/search/index.tsx index 993dfb0513..d7c07b753d 100644 --- a/app/screens/home/search/index.tsx +++ b/app/screens/home/search/index.tsx @@ -10,20 +10,19 @@ import {Edge, SafeAreaView} from 'react-native-safe-area-context'; import FreezeScreen from '@components/freeze_screen'; import NavigationHeader from '@components/navigation_header'; +import RoundedHeaderContext from '@components/rounded_header_context'; import {useCollapsibleHeader} from '@hooks/header'; // import RecentSearches from './recent_searches/recent_searches'; // import SearchModifiers from './search_modifiers/search_modifiers'; // import Filter from './results/filter'; -import {SelectTab} from './results/header'; +import Header, {SelectTab} from './results/header'; import Results from './results/results'; const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView); const EDGES: Edge[] = ['bottom', 'left', 'right']; -const TOP_MARGIN = 12; - const SearchScreen = () => { const nav = useNavigation(); const isFocused = useIsFocused(); @@ -35,10 +34,6 @@ const SearchScreen = () => { const [searchValue, setSearchValue] = useState(searchTerm); const [selectedTab, setSelectedTab] = useState('messages'); - useEffect(() => { - setSearchValue(searchTerm); - }, [searchTerm]); - const handleSearch = () => { // execute the search for the text in the navigation text box // handle recent searches @@ -48,14 +43,16 @@ const SearchScreen = () => { // console.log('execute the search for : ', searchValue); }; - const isLargeTitle = true; - const hasSearch = true; + const onSnap = (y: number) => { + scrollRef.current?.scrollTo({y, animated: true}); + }; - const {scrollPaddingTop, scrollRef, scrollValue, onScroll, hideHeader} = useCollapsibleHeader(isLargeTitle, false, hasSearch); + useEffect(() => { + setSearchValue(searchTerm); + }, [searchTerm]); - const onHeaderTabSelect = useCallback((tab: SelectTab) => { - setSelectedTab(tab); - }, [setSelectedTab]); + const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader(true, onSnap); + const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop, flexGrow: 1}), [scrollPaddingTop]); const animated = useAnimatedStyle(() => { if (isFocused) { @@ -75,19 +72,28 @@ const SearchScreen = () => { }; }, [isFocused, stateIndex, scrollPaddingTop]); - const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop + TOP_MARGIN, flexGrow: 1}), [scrollPaddingTop]); + const top = useAnimatedStyle(() => { + return { + top: headerHeight.value, + zIndex: searchValue ? 10 : 0, + }; + }, [searchValue]); + + const onHeaderTabSelect = useCallback((tab: SelectTab) => { + setSelectedTab(tab); + }, [setSelectedTab]); return ( { // eslint-disable-next-line no-console console.log('BACK'); }} showBackButton={false} title={intl.formatMessage({id: 'screen.search.title', defaultMessage: 'Search'})} - hasSearch={hasSearch} + hasSearch={true} scrollValue={scrollValue} hideHeader={hideHeader} onChangeText={setSearchValue} @@ -101,6 +107,16 @@ const SearchScreen = () => { edges={EDGES} > + + + {Boolean(searchValue) && +
+ } + { indicatorStyle='black' onScroll={onScroll} scrollEventThrottle={16} + removeClippedSubviews={true} ref={scrollRef} > {/* { {/* */} diff --git a/app/screens/home/search/results/header.tsx b/app/screens/home/search/results/header.tsx index f11ed8f93a..407772cb7e 100644 --- a/app/screens/home/search/results/header.tsx +++ b/app/screens/home/search/results/header.tsx @@ -17,13 +17,17 @@ type Props = { numberMessages: number; } -const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { +export const HEADER_HEIGHT = 64; + +const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => { return { container: { + backgroundColor: theme.centerChannelBg, marginHorizontal: 12, flexDirection: 'row', - marginBottom: 12, + paddingVertical: 12, flexGrow: 0, + height: HEADER_HEIGHT, }, divider: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.08), diff --git a/app/screens/home/search/results/header_button.tsx b/app/screens/home/search/results/header_button.tsx index 14785a5858..c2e742a7c7 100644 --- a/app/screens/home/search/results/header_button.tsx +++ b/app/screens/home/search/results/header_button.tsx @@ -20,8 +20,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { height: 40, }, text: { - marginHorizontal: 16, - marginVertical: 8, + paddingHorizontal: 16, + paddingVertical: 8, ...typography('Body', 200, 'SemiBold'), }, selectedButton: { diff --git a/app/screens/home/search/results/results.tsx b/app/screens/home/search/results/results.tsx index dc65c80bbe..6c30a6155a 100644 --- a/app/screens/home/search/results/results.tsx +++ b/app/screens/home/search/results/results.tsx @@ -6,12 +6,9 @@ import {Text, View} from 'react-native'; import NoResultsWithTerm from '@components/no_results_with_term'; -import Header from './header'; - type Props = { searchValue: string; selectedTab: string; - onHeaderTabSelect: (tab: string) => void; } const emptyPostResults: Post[] = []; @@ -20,20 +17,18 @@ const emptyFilesResults: FileInfo[] = []; const notImplementedComponent = ( - {'Not Implemented'} + {'Not Implemented'} ); const SearchResults = ({ searchValue, selectedTab, - onHeaderTabSelect, }: Props) => { const [postResults] = useState(emptyPostResults); const [fileResults] = useState(emptyFilesResults); @@ -48,17 +43,19 @@ const SearchResults = ({ (selectedTab === 'messages' && postResults.length === 0) || (selectedTab === 'files' && fileResults.length === 0) ) { - content = (<> -
- - + content = ( + + + ); } else { content = notImplementedComponent;