forked from Ivasoft/mattermost-mobile
styling mobile
wip
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
56
app/constants/snack_bar.ts
Normal file
56
app/constants/snack_bar.ts
Normal 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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
113
app/screens/snack_bar/index.tsx
Normal file
113
app/screens/snack_bar/index.tsx
Normal 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;
|
||||
16
app/utils/snack_bar/index.ts
Normal file
16
app/utils/snack_bar/index.ts
Normal 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);
|
||||
};
|
||||
Reference in New Issue
Block a user