forked from Ivasoft/mattermost-mobile
[Gekidou] Navigation bar refactored (#6319)
* Navigation bar refactored * feedback review * add MAX_OVERSCROLL constant
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -20,8 +20,8 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
|
||||
height: 40,
|
||||
},
|
||||
text: {
|
||||
marginHorizontal: 16,
|
||||
marginVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
...typography('Body', 200, 'SemiBold'),
|
||||
},
|
||||
selectedButton: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user