[Gekidou] Navigation bar refactored (#6319)

* Navigation bar refactored

* feedback review

* add MAX_OVERSCROLL constant
This commit is contained in:
Elias Nahum
2022-06-01 17:07:54 -04:00
committed by GitHub
parent f203b74b2f
commit 62d2e20441
16 changed files with 253 additions and 282 deletions

View File

@@ -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<number>;
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 (
<Animated.View style={marginTop}>
<RoundedHeaderContext/>
</Animated.View>
);
};
export default NavigationHeaderContext;

View File

@@ -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]);

View File

@@ -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<number>;
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 &&
<NavigationSearch
{...searchProps}
defaultHeight={defaultHeight}
largeHeight={largeHeight}
scrollValue={scrollValue}
hideHeader={hideHeader}
theme={theme}
top={0}
/>
}
</Animated.View>
{hasSearch &&
<>
<NavigationSearch
{...searchProps}
largeHeight={largeHeight}
scrollValue={scrollValue}
hideHeader={hideHeader}
theme={theme}
top={insets.top}
/>
<NavigationHeaderSearchContext
defaultHeight={defaultHeight}
largeHeight={largeHeight}
scrollValue={scrollValue}
theme={theme}
/>
</>
}
{showHeaderInContext &&
<NavigationHeaderContext
defaultHeight={defaultHeight}
hasSearch={hasSearch}
isLargeTitle={isLargeTitle}
largeHeight={largeHeight}
scrollValue={scrollValue}
top={insets.top}
/>
}
</>
);
};

View File

@@ -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];

View File

@@ -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<number>;
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 (
<Animated.View style={[styles.container, searchTop]}>

View File

@@ -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<number>;
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 (
<Animated.View style={[styles.container, marginTop]}/>
);
};
export default NavigationHeaderSearchContext;

View File

@@ -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',
},

View File

@@ -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 = <T>(isLargeTitle: boolean, hasSubtitle: boolean, hasSearch: boolean) => {
export const useCollapsibleHeader = <T>(isLargeTitle: boolean, onSnap?: (offset: number) => void) => {
const insets = useSafeAreaInsets();
const animatedRef = useAnimatedRef<Animated.ScrollView>();
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 = <T>(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<T>,
scrollValue,
onScroll,
hideHeader,
headerHeight,
};
};

View File

@@ -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 (
<NavigationHeader
isLargeTitle={false}
leftComponent={leftComponent}
onBackPress={onBackPress}
onTitlePress={onTitlePress}
rightButtons={rightButtons}
showBackButton={!isTablet}
subtitle={subtitle}
subtitleCompanion={subtitleCompanion}
title={title}
/>
<>
<NavigationHeader
isLargeTitle={false}
leftComponent={leftComponent}
onBackPress={onBackPress}
onTitlePress={onTitlePress}
rightButtons={rightButtons}
showBackButton={!isTablet}
subtitle={subtitle}
subtitleCompanion={subtitleCompanion}
title={title}
/>
<View style={contextStyle}>
<RoundedHeaderContext/>
</View>
</>
);
};

View File

@@ -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);

View File

@@ -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) => {
})
}
/>
<View style={contextStyle}>
<RoundedHeaderContext/>
</View>
{!switchingTeam &&
<View style={containerStyle}>
<ThreadsList

View File

@@ -6,13 +6,14 @@ import React, {useCallback, useState, useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {ActivityIndicator, DeviceEventEmitter, FlatList, StyleSheet, View} from 'react-native';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import {SafeAreaView, Edge} from 'react-native-safe-area-context';
import {SafeAreaView, Edge, useSafeAreaInsets} from 'react-native-safe-area-context';
import {fetchRecentMentions} from '@actions/remote/search';
import FreezeScreen from '@components/freeze_screen';
import NavigationHeader from '@components/navigation_header';
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 {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
@@ -25,7 +26,6 @@ import type {ViewableItemsChanged} from '@typings/components/post_list';
import type PostModel from '@typings/database/models/servers/post';
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
const EDGES: Edge[] = ['bottom', 'left', 'right'];
type Props = {
@@ -49,6 +49,7 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
const theme = useTheme();
const route = useRoute();
const isFocused = useIsFocused();
const insets = useSafeAreaInsets();
const {formatMessage} = useIntl();
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(true);
@@ -62,7 +63,15 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
const title = formatMessage({id: 'screen.mentions.title', defaultMessage: 'Recent Mentions'});
const subtitle = formatMessage({id: 'screen.mentions.subtitle', defaultMessage: 'Messages you\'ve been mentioned in'});
const isLargeTitle = true;
const onSnap = (offset: number) => {
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<FlatList<string>>(isLargeTitle, Boolean(subtitle), false);
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop}), [scrollPaddingTop]);
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight} = useCollapsibleHeader<FlatList<string>>(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(() => (
<View style={[styles.empty, paddingTop]}>
<View style={styles.empty}>
{loading ? (
<ActivityIndicator
color={theme.centerChannelColor}
@@ -148,7 +157,7 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
return (
<FreezeScreen freeze={!isFocused}>
<NavigationHeader
isLargeTitle={isLargeTitle}
isLargeTitle={true}
showBackButton={false}
subtitle={subtitle}
title={title}
@@ -160,6 +169,9 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
edges={EDGES}
>
<Animated.View style={[styles.flex, animated]}>
<Animated.View style={top}>
<RoundedHeaderContext/>
</Animated.View>
<AnimatedFlatList
ref={scrollRef}
contentContainerStyle={paddingTop}
@@ -174,7 +186,9 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
onRefresh={handleRefresh}
refreshing={refreshing}
renderItem={renderItem}
removeClippedSubviews={true}
onViewableItemsChanged={onViewableItemsChanged}
style={scrollViewStyle}
/>
</Animated.View>
</SafeAreaView>

View File

@@ -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<string>(searchTerm);
const [selectedTab, setSelectedTab] = useState<string>('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<ScrollView>(isLargeTitle, false, hasSearch);
useEffect(() => {
setSearchValue(searchTerm);
}, [searchTerm]);
const onHeaderTabSelect = useCallback((tab: SelectTab) => {
setSelectedTab(tab);
}, [setSelectedTab]);
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight, hideHeader} = useCollapsibleHeader<ScrollView>(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 (
<FreezeScreen freeze={!isFocused}>
<NavigationHeader
isLargeTitle={isLargeTitle}
isLargeTitle={true}
onBackPress={() => {
// 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}
>
<Animated.View style={animated}>
<Animated.View style={top}>
<RoundedHeaderContext/>
{Boolean(searchValue) &&
<Header
onTabSelect={onHeaderTabSelect}
numberFiles={0}
numberMessages={0}
/>
}
</Animated.View>
<AnimatedScrollView
contentContainerStyle={paddingTop}
nestedScrollEnabled={true}
@@ -109,6 +125,7 @@ const SearchScreen = () => {
indicatorStyle='black'
onScroll={onScroll}
scrollEventThrottle={16}
removeClippedSubviews={true}
ref={scrollRef}
>
{/* <SearchModifiers */}
@@ -121,7 +138,6 @@ const SearchScreen = () => {
<Results
selectedTab={selectedTab}
searchValue={searchValue}
onHeaderTabSelect={onHeaderTabSelect}
/>
{/* <Filter/> */}
</AnimatedScrollView>

View File

@@ -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),

View File

@@ -20,8 +20,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
height: 40,
},
text: {
marginHorizontal: 16,
marginVertical: 8,
paddingHorizontal: 16,
paddingVertical: 8,
...typography('Body', 200, 'SemiBold'),
},
selectedButton: {

View File

@@ -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 = (
<View
style={{
height: 400,
height: 800,
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text>{'Not Implemented'}</Text>
<Text style={{fontSize: 28, color: '#000'}}>{'Not Implemented'}</Text>
</View>
);
const SearchResults = ({
searchValue,
selectedTab,
onHeaderTabSelect,
}: Props) => {
const [postResults] = useState<Post[]>(emptyPostResults);
const [fileResults] = useState<FileInfo[]>(emptyFilesResults);
@@ -48,17 +43,19 @@ const SearchResults = ({
(selectedTab === 'messages' && postResults.length === 0) ||
(selectedTab === 'files' && fileResults.length === 0)
) {
content = (<>
<Header
onTabSelect={onHeaderTabSelect}
numberFiles={0}
numberMessages={0}
/>
<NoResultsWithTerm
term={searchValue}
type={selectedTab}
/>
</>
content = (
<View
style={{
height: 800,
flexGrow: 1,
alignItems: 'center',
}}
>
<NoResultsWithTerm
term={searchValue}
type={selectedTab}
/>
</View>
);
} else {
content = notImplementedComponent;