[GEKIDOU] Bottom Tab Navigation (#5600)

* Started with bottom tabs layout

* code clean up

* Added animation to bottom tab bar

* returns null if not focused

* code clean up

* Updating layout

* Updated modal screen

* Updated animation

* Updated animation

* Fix SafeArea on Home

* A few clean ups

* code clean up

* Fix issue with navigation on Android

* Use React Navigation in combination of RNN & create bottom tab bar

* Set tab bar line separator height to 0.5

* Fix snapshot tests

* Add home tab mention badge

* Apply new themes

* Home Tab badge

* Remove unused constants

Co-authored-by: Avinash Lingaloo <>
Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Avinash Lingaloo
2021-08-31 22:58:53 +04:00
committed by GitHub
parent b5c5949b7d
commit 2c193f2133
56 changed files with 4454 additions and 2674 deletions

View File

@@ -0,0 +1,117 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as React from 'react';
import {Animated, StyleProp, StyleSheet, TextStyle} from 'react-native';
import {useTheme} from '@context/theme';
type Props = {
/**
* Whether the badge is visible
*/
visible: boolean;
/**
* Content of the `Badge`.
*/
children?: string | number;
/**
* Size of the `Badge`.
*/
size?: number;
/**
* Style object for the tab bar container.
*/
style?: Animated.WithAnimatedValue<StyleProp<TextStyle>>;
};
export default function Badge({
visible = true,
size = 18,
children,
style,
...rest
}: Props) {
const [opacity] = React.useState(() => new Animated.Value(visible ? 1 : 0));
const [rendered, setRendered] = React.useState(Boolean(visible));
const theme = useTheme();
React.useEffect(() => {
if (!rendered) {
return;
}
Animated.timing(opacity, {
toValue: visible ? 1 : 0,
duration: 150,
useNativeDriver: true,
}).start(({finished}) => {
if (finished && !visible) {
setRendered(false);
}
});
}, [opacity, rendered, visible]);
if (visible && !rendered) {
setRendered(true);
}
if (!visible && !rendered) {
return null;
}
// @ts-expect-error: backgroundColor definitely exists
const {backgroundColor = theme.buttonBg, ...restStyle} =
StyleSheet.flatten(style) || {};
const textColor = theme.buttonColor;
const borderRadius = size / 2;
const fontSize = Math.floor((size * 3) / 4);
return (
<Animated.Text
numberOfLines={1}
style={[
{
opacity,
transform: [
{
scale: opacity.interpolate({
inputRange: [0, 1],
outputRange: [0.5, 1],
}),
},
],
backgroundColor,
color: textColor,
fontSize,
lineHeight: size - 1,
height: size,
minWidth: size,
borderRadius,
},
styles.container,
restStyle,
]}
{...rest}
>
{children}
</Animated.Text>
);
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: -1,
left: 15,
alignSelf: 'flex-end',
textAlign: 'center',
paddingHorizontal: 4,
overflow: 'hidden',
},
});

View File

@@ -5,7 +5,7 @@ exports[`ErrorText should match snapshot 1`] = `
style={
Array [
Object {
"color": "#fd5960",
"color": "#d24b4e",
"fontSize": 12,
"marginBottom": 15,
"marginTop": 15,

View File

@@ -15,7 +15,7 @@ describe('ErrorText', () => {
fontSize: 14,
marginHorizontal: 15,
},
theme: Preferences.THEMES.default,
theme: Preferences.THEMES.denim,
error: 'Username must begin with a letter and contain between 3 and 22 characters including numbers, lowercase letters, and the symbols',
};

View File

@@ -13,9 +13,9 @@ import FormattedText from '@components/formatted_text';
import ProgressiveImage from '@components/progressive_image';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {Navigation} from '@constants';
import {Device, Navigation} from '@constants';
import {useServerUrl} from '@context/server_url';
import {usePermanentSidebar, useSplitView} from '@hooks/device';
import {useSplitView} from '@hooks/device';
import {showModalOverCurrentContext} from '@screens/navigation';
import {openGallerWithMockFile} from '@utils/gallery';
import {generateId} from '@utils/general';
@@ -55,7 +55,6 @@ const MarkdownImage = ({
const intl = useIntl();
const isSplitView = useSplitView();
const managedConfig = useManagedConfig();
const permanentSidebar = usePermanentSidebar();
const genericFileId = useRef(generateId()).current;
const metadata = imagesMetadata?.[source] || Object.values(imagesMetadata || {})[0];
const [failed, setFailed] = useState(isGifTooLarge(metadata));
@@ -87,7 +86,7 @@ const MarkdownImage = ({
height: originalSize.height,
};
const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, getViewPortWidth(isReplyPost, (permanentSidebar && !isSplitView)));
const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, getViewPortWidth(isReplyPost, Device.IS_TABLET && !isSplitView));
const handleLinkPress = useCallback(() => {
if (linkDestination) {

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import {Platform, StyleProp, View, ViewProps, ViewStyle} from 'react-native';
import FastImage from 'react-native-fast-image';
// import {fetchStatusInBatch} from '@actions/remote/user';
// import UserStatus from '@components/user_status';
import CompassIcon from '@components/compass_icon';
import {useServerUrl} from '@context/server_url';
import {useTheme} from '@context/theme';
import NetworkManager from '@init/network_manager';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import type {Client} from '@client/rest';
import type UserModel from '@typings/database/models/servers/user';
const STATUS_BUFFER = Platform.select({
ios: 3,
android: 2,
});
type ProfilePictureProps = {
author?: UserModel;
iconSize?: number;
showStatus?: boolean;
size: number;
statusSize?: number;
statusStyle?: StyleProp<ViewProps>;
testID?: string;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
container: {
justifyContent: 'center',
alignItems: 'center',
borderRadius: 80,
},
icon: {
color: changeOpacity(theme.centerChannelColor, 0.48),
},
statusWrapper: {
position: 'absolute',
bottom: 0,
right: 0,
overflow: 'hidden',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: theme.centerChannelBg,
borderWidth: 1,
borderColor: theme.centerChannelBg,
},
status: {
color: theme.centerChannelBg,
},
};
});
const ProfilePicture = ({author, iconSize, showStatus = true, size = 64, statusSize = 14, statusStyle, testID}: ProfilePictureProps) => {
const theme = useTheme();
const serverUrl = useServerUrl();
const style = getStyleSheet(theme);
const buffer = showStatus ? (STATUS_BUFFER || 0) : 0;
let client: Client | undefined;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
// handle below that the client is not set
}
useEffect(() => {
if (author && !author.status && showStatus) {
// fetchStatusInBatch(serverUrl, author.id);
}
}, []);
let statusIcon;
let containerStyle: StyleProp<ViewStyle> = {
width: size + buffer,
height: size + buffer,
};
if (author?.status && !author.isBot && showStatus) {
statusIcon = (
<View style={[style.statusWrapper, statusStyle, {borderRadius: statusSize / 2}]}>
{/* <UserStatus
size={statusSize}
status={author.status}
/> */}
</View>
);
}
let image;
if (author && client) {
const pictureUrl = client.getProfilePictureUrl(author.id, author.lastPictureUpdate);
image = (
<FastImage
key={pictureUrl}
style={{width: size, height: size, borderRadius: (size / 2)}}
source={{uri: `${serverUrl}${pictureUrl}`}}
/>
);
} else {
containerStyle = {
width: size + (buffer - 1),
height: size + (buffer - 1),
backgroundColor: changeOpacity(theme.centerChannelColor, 0.08),
};
image = (
<CompassIcon
name='account-outline'
size={iconSize || size}
style={style.icon}
/>
);
}
return (
<View
style={[style.container, containerStyle]}
testID={`${testID}.${author?.id}`}
>
{image}
{statusIcon}
</View>
);
};
export default ProfilePicture;