forked from Ivasoft/mattermost-mobile
[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:
117
app/components/badge/index.tsx
Normal file
117
app/components/badge/index.tsx
Normal 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',
|
||||
},
|
||||
});
|
||||
@@ -5,7 +5,7 @@ exports[`ErrorText should match snapshot 1`] = `
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#fd5960",
|
||||
"color": "#d24b4e",
|
||||
"fontSize": 12,
|
||||
"marginBottom": 15,
|
||||
"marginTop": 15,
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
133
app/components/profile_picture/index.tsx
Normal file
133
app/components/profile_picture/index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user