styling mobile

wip
This commit is contained in:
Avinash Lingaloo
2022-04-19 16:10:37 +04:00
parent 1996224a4c
commit 3ef280c80c
12 changed files with 266 additions and 32 deletions

View File

@@ -3,7 +3,17 @@
import {FlatList} from '@stream-io/flat-list-mvcp';
import React, {ReactElement, useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {DeviceEventEmitter, NativeScrollEvent, NativeSyntheticEvent, Platform, StyleProp, StyleSheet, ViewStyle} from 'react-native';
import {
DeviceEventEmitter,
LayoutChangeEvent,
NativeScrollEvent,
NativeSyntheticEvent,
Platform,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native';
import Animated from 'react-native-reanimated';
import {fetchPosts, fetchPostThread} from '@actions/remote/post';
@@ -111,6 +121,12 @@ const PostList = ({
return orderedPosts.indexOf(START_OF_NEW_MESSAGES);
}, [orderedPosts]);
const [offSetY, setOffSetY] = useState(0);
const onLayout = useCallback((layoutEvent: LayoutChangeEvent) => {
const {layout} = layoutEvent.nativeEvent;
setOffSetY(layout.y);
}, []);
useEffect(() => {
listRef.current?.scrollToOffset({offset: 0, animated: false});
}, [channelId]);
@@ -296,6 +312,7 @@ const PostList = ({
previousPost,
shouldRenderReplyButton,
skipSaveddHeader,
offSetY,
};
return (
@@ -307,7 +324,7 @@ const PostList = ({
{...postProps}
/>
);
}, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]);
}, [currentTimezone, highlightPinnedOrSaved, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme, offSetY]);
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
listRef.current?.scrollToIndex({
@@ -366,6 +383,7 @@ const PostList = ({
testID={`${testID}.flat_list`}
/>
</PostListRefreshControl>
<View onLayout={onLayout}/>
{showMoreMessages &&
<MoreMessages
channelId={channelId}

View File

@@ -55,6 +55,7 @@ type PostProps = {
skipPinnedHeader?: boolean;
style?: StyleProp<ViewStyle>;
testID?: string;
offSetY: number;
};
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -99,7 +100,7 @@ const Post = ({
appsEnabled, canDelete, currentUser, differentThreadSequence, filesCount, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember,
location, post, reactionsCount, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
testID, previousPost,
testID, previousPost, offSetY,
}: PostProps) => {
const pressDetected = useRef(false);
const intl = useIntl();
@@ -166,7 +167,7 @@ const Post = ({
}
Keyboard.dismiss();
const passProps = {location, post, showAddReaction};
const passProps = {location, post, showAddReaction, offSetY};
const title = isTablet ? intl.formatMessage({id: 'post.options.title', defaultMessage: 'Options'}) : '';
if (isTablet) {

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleProp, Text, useWindowDimensions, View, ViewStyle} from 'react-native';
import {StyleProp, Text, TextStyle, useWindowDimensions, View, ViewStyle} from 'react-native';
import Animated, {AnimatedStyleProp} from 'react-native-reanimated';
import CompassIcon from '@components/compass_icon';
@@ -15,7 +15,8 @@ type ToastProps = {
children?: React.ReactNode;
iconName?: string;
message?: string;
style: StyleProp<ViewStyle>;
style?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -46,7 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const Toast = ({animatedStyle, children, style, iconName, message}: ToastProps) => {
const Toast = ({animatedStyle, children, style, iconName, message, textStyle}: ToastProps) => {
const theme = useTheme();
const styles = getStyleSheet(theme);
const dim = useWindowDimensions();
@@ -65,11 +66,12 @@ const Toast = ({animatedStyle, children, style, iconName, message}: ToastProps)
color={theme.buttonColor}
name={iconName!}
size={18}
style={textStyle}
/>
}
{Boolean(message) &&
<View style={styles.flex}>
<Text style={styles.text}>{message}</Text>
<Text style={[styles.text, textStyle]}>{message}</Text>
</View>
}
{children}

View File

@@ -24,6 +24,7 @@ import Preferences from './preferences';
import Profile from './profile';
import Screens from './screens';
import ServerErrors from './server_errors';
import SnackBar from './snack_bar';
import Sso from './sso';
import SupportedServer from './supported_server';
import View from './view';
@@ -33,9 +34,9 @@ export {
ActionType,
Apps,
Categories,
Channel,
Config,
CustomStatusDuration,
Channel,
Database,
DeepLink,
Device,
@@ -53,8 +54,9 @@ export {
Profile,
Screens,
ServerErrors,
SupportedServer,
SnackBar,
Sso,
SupportedServer,
View,
WebsocketEvents,
};

View File

@@ -39,6 +39,7 @@ export const SSO = 'SSO';
export const THREAD = 'Thread';
export const THREAD_FOLLOW_BUTTON = 'ThreadFollowButton';
export const USER_PROFILE = 'UserProfile';
export const SNACK_BAR = 'SnackBar';
export default {
ABOUT,
@@ -79,6 +80,7 @@ export default {
THREAD,
THREAD_FOLLOW_BUTTON,
USER_PROFILE,
SNACK_BAR,
};
export const MODAL_SCREENS_WITHOUT_BACK = [

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {t} from '@i18n';
import keyMirror from '@utils/key_mirror';
export const SNACK_BAR_TYPE = keyMirror({
LINK_COPIED: null,
MESSAGE_COPIED: null,
FOLLOW_THREAD: null,
MUTE_CHANNEL: null,
FAILED_TO_SAVE_MESSAGE: null,
});
type SnackBarConfig = {
id: string;
defaultMessage: string;
iconName: string;
themeColor: string;
canUndo: boolean;
}
export const SNACK_BAR_CONFIG: Record<string, SnackBarConfig> = {
LINK_COPIED: {
id: t('snack.bar.link.copied'),
defaultMessage: 'Link copied to clipboard',
iconName: 'check',
themeColor: 'onlineIndicator',
canUndo: false,
},
MESSAGE_COPIED: {
id: t('snack.bar.message.copied'),
defaultMessage: 'Message copied to clipboard',
iconName: 'content-copy',
themeColor: 'centerChannelColor',
canUndo: false,
},
FOLLOW_THREAD: {
id: t('snack.bar.follow.thread'),
defaultMessage: 'You\'re now following this thread',
iconName: 'message-check-outline',
themeColor: 'centerChannelColor',
canUndo: true,
},
MUTE_CHANNEL: {
id: t('snack.bar.mute.channel'),
defaultMessage: 'This channel was muted',
iconName: 'bell-off-outline',
themeColor: 'centerChannelColor',
canUndo: true,
},
};
export default {
SNACK_BAR_TYPE,
SNACK_BAR_CONFIG,
};

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {withManagedConfig} from '@mattermost/react-native-emm';
import React from 'react';
import React, {ComponentType} from 'react';
import {IntlProvider} from 'react-intl';
import {Platform, StyleProp, ViewStyle} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
@@ -153,6 +153,17 @@ Navigation.setLazyComponentRegistrator((screenName) => {
require('@screens/thread/thread_follow_button').default,
));
break;
case Screens.SNACK_BAR: {
const snackBarScreen = withServerDatabase(require('@screens/snack_bar').default);
Navigation.registerComponent(Screens.SNACK_BAR, () =>
Platform.select({
default: snackBarScreen,
ios: withSafeAreaInsets(snackBarScreen) as ComponentType,
}),
);
break;
}
}
if (screen) {

View File

@@ -5,25 +5,30 @@ import Clipboard from '@react-native-community/clipboard';
import React, {useCallback} from 'react';
import {Screens} from '@constants';
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
import {useServerUrl} from '@context/server';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import {showSnackBar} from '@utils/snack_bar';
import BaseOption from '../base_option';
import type PostModel from '@typings/database/models/servers/post';
type Props = {
teamName: string;
location: typeof Screens[keyof typeof Screens];
post: PostModel;
teamName: string;
offSetY: number;
}
const CopyPermalinkOption = ({teamName, post}: Props) => {
const CopyPermalinkOption = ({teamName, post, location, offSetY}: Props) => {
const serverUrl = useServerUrl();
const handleCopyLink = useCallback(() => {
const handleCopyLink = useCallback(async () => {
const permalink = `${serverUrl}/${teamName}/pl/${post.id}`;
Clipboard.setString(permalink);
dismissBottomSheet(Screens.POST_OPTIONS);
await dismissBottomSheet(Screens.POST_OPTIONS);
showSnackBar({barType: SNACK_BAR_TYPE.LINK_COPIED, location, offSetY});
}, [teamName, post.id]);
return (

View File

@@ -5,18 +5,23 @@ import Clipboard from '@react-native-community/clipboard';
import React, {useCallback} from 'react';
import {Screens} from '@constants';
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
import {t} from '@i18n';
import {dismissBottomSheet} from '@screens/navigation';
import {showSnackBar} from '@utils/snack_bar';
import BaseOption from './base_option';
type Props = {
location: typeof Screens[keyof typeof Screens];
postMessage: string;
offSetY: number;
}
const CopyTextOption = ({postMessage}: Props) => {
const handleCopyText = useCallback(() => {
const CopyTextOption = ({postMessage, location, offSetY}: Props) => {
const handleCopyText = useCallback(async () => {
Clipboard.setString(postMessage);
dismissBottomSheet(Screens.POST_OPTIONS);
await dismissBottomSheet(Screens.POST_OPTIONS);
showSnackBar({barType: SNACK_BAR_TYPE.MESSAGE_COPIED, location, offSetY});
}, [postMessage]);
return (

View File

@@ -37,21 +37,14 @@ type PostOptionsProps = {
post: PostModel;
thread: Partial<PostModel>;
componentId: string;
offSetY: number;
};
const PostOptions = ({
canAddReaction,
canDelete,
canEdit,
canMarkAsUnread,
canPin,
canReply,
combinedPost,
componentId,
isSaved,
location,
post,
thread,
canAddReaction, canDelete, canEdit,
canMarkAsUnread, canPin, canReply,
combinedPost, componentId, isSaved,
location, post, thread, offSetY,
}: PostOptionsProps) => {
const managedConfig = useManagedConfig<ManagedConfig>();
@@ -101,14 +94,24 @@ const PostOptions = ({
{canMarkAsUnread && !isSystemPost &&
<MarkAsUnreadOption postId={post.id}/>
}
{canCopyPermalink && <CopyLinkOption post={post}/>}
{Boolean(canCopyPermalink && post.message) &&
<CopyLinkOption
post={post}
location={location}
offSetY={offSetY}
/>}
{!isSystemPost &&
<SaveOption
isSaved={isSaved}
postId={post.id}
/>
}
{Boolean(canCopyText && post.message) && <CopyTextOption postMessage={post.message}/>}
{Boolean(canCopyText && post.message) &&
<CopyTextOption
postMessage={post.message}
location={location}
offSetY={offSetY}
/>}
{canPin &&
<PinChannelOption
isPostPinned={post.isPinned}

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {Text, TouchableOpacity} from 'react-native';
import {useAnimatedStyle, withTiming} from 'react-native-reanimated';
import Toast from '@components/toast';
import {Screens} from '@constants';
import {SNACK_BAR_CONFIG, SNACK_BAR_TYPE} from '@constants/snack_bar';
import {BOTTOM_TAB_HEIGHT} from '@constants/view';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {dismissOverlay} from '@screens/navigation';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
text: {
color: theme.centerChannelBg,
},
undo: {
color: theme.centerChannelBg,
...typography('Body', 100, 'SemiBold'),
},
};
});
type SnackBarProps = {
componentId: string;
onPress?: () => void;
barType: keyof typeof SNACK_BAR_TYPE;
location: typeof Screens[keyof typeof Screens];
}
const SnackBar = ({barType, componentId, onPress, location}: SnackBarProps) => {
const [showToast, setShowToast] = useState<boolean | undefined>();
const intl = useIntl();
const theme = useTheme();
const isTablet = useIsTablet();
const config = SNACK_BAR_CONFIG[barType];
const styles = getStyleSheet(theme);
const onPressHandler = useCallback(() => {
dismissOverlay(componentId);
onPress?.();
}, [onPress, componentId]);
const animatedStyle = useAnimatedStyle(() => {
let diff = 50;
const screens = [Screens.PERMALINK, Screens.MENTIONS, Screens.SAVED_POSTS];
if (!isTablet && screens.includes(location)) {
diff = 7;
}
return {
position: 'absolute',
bottom: BOTTOM_TAB_HEIGHT + diff,
opacity: withTiming(showToast ? 1 : 0, {duration: 300}),
};
});
useEffect(() => {
setShowToast(true);
const t = setTimeout(() => {
setShowToast(false);
}, 3000);
return () => clearTimeout(t);
}, []);
useEffect(() => {
let t: NodeJS.Timeout;
if (showToast === false) {
t = setTimeout(() => {
dismissOverlay(componentId);
}, 350);
}
return () => {
if (t) {
clearTimeout(t);
}
};
}, [showToast]);
return (
<Toast
animatedStyle={animatedStyle}
style={{}}
message={intl.formatMessage({id: config.id, defaultMessage: config.defaultMessage})}
iconName={config.iconName}
textStyle={styles.text}
>
{config.canUndo && onPress && (
<TouchableOpacity onPress={onPressHandler}>
<Text
style={styles.undo}
>
{intl.formatMessage({
id: 'snack.bar.undo',
defaultMessage: 'Undo',
})}
</Text>
</TouchableOpacity>)}
</Toast>
);
};
export default SnackBar;

View File

@@ -0,0 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Screens} from '@constants';
import {SNACK_BAR_TYPE} from '@constants/snack_bar';
import {showOverlay} from '@screens/navigation';
type ShowSnackBarArgs = {
barType: keyof typeof SNACK_BAR_TYPE;
onPress?: () => void;
location?: typeof Screens[keyof typeof Screens];
offSetY?: number;
};
export const showSnackBar = (passProps: ShowSnackBarArgs) => {
const screen = Screens.SNACK_BAR;
showOverlay(screen, passProps);
};