[Gekidou] Add saved messages to bottom tab (#6367)
This commit is contained in:
parent
ecd877b5e4
commit
6906a9f863
19 changed files with 303 additions and 317 deletions
|
|
@ -111,7 +111,7 @@ const Body = ({
|
|||
}, []);
|
||||
|
||||
const onLayout = useCallback((e: LayoutChangeEvent) => {
|
||||
if (location === Screens.SAVED_POSTS) {
|
||||
if (location === Screens.SAVED_MESSAGES) {
|
||||
setLayoutWidth(e.nativeEvent.layout.width);
|
||||
}
|
||||
}, [location]);
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const Post = ({
|
|||
}, [isConsecutivePost, post, previousPost, isFirstReply]);
|
||||
|
||||
const handlePostPress = () => {
|
||||
if ([Screens.SAVED_POSTS, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
|
||||
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
|
||||
showPermalink(serverUrl, '', post.id, intl);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const PERMALINK = 'Permalink';
|
|||
export const PINNED_MESSAGES = 'PinnedMessages';
|
||||
export const POST_OPTIONS = 'PostOptions';
|
||||
export const REACTIONS = 'Reactions';
|
||||
export const SAVED_POSTS = 'SavedPosts';
|
||||
export const SAVED_MESSAGES = 'SavedMessages';
|
||||
export const SEARCH = 'Search';
|
||||
export const SELECT_TEAM = 'SelectTeam';
|
||||
export const SERVER = 'Server';
|
||||
|
|
@ -91,7 +91,7 @@ export default {
|
|||
PINNED_MESSAGES,
|
||||
POST_OPTIONS,
|
||||
REACTIONS,
|
||||
SAVED_POSTS,
|
||||
SAVED_MESSAGES,
|
||||
SEARCH,
|
||||
SELECT_TEAM,
|
||||
SERVER,
|
||||
|
|
@ -124,7 +124,6 @@ export const MODAL_SCREENS_WITHOUT_BACK = [
|
|||
GALLERY,
|
||||
PERMALINK,
|
||||
REACTIONS,
|
||||
SAVED_POSTS,
|
||||
];
|
||||
|
||||
export const NOT_READY = [
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@
|
|||
|
||||
import React, {useCallback, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, Keyboard, Platform, Text, View} from 'react-native';
|
||||
import {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 {General, Navigation, Screens} from '@constants';
|
||||
import {General, Screens} from '@constants';
|
||||
import {QUICK_OPTIONS_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
|
|
@ -140,19 +140,22 @@ const ChannelHeader = ({
|
|||
});
|
||||
}, [channelId, channelType, isTablet, onTitlePress, theme]);
|
||||
|
||||
const rightButtons: HeaderRightButton[] = useMemo(() => ([{
|
||||
iconName: 'magnify',
|
||||
onPress: () => {
|
||||
DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {screen: 'Search', params: {searchTerm: `in: ${searchTerm}`}});
|
||||
if (!isTablet) {
|
||||
popTopScreen(componentId);
|
||||
}
|
||||
},
|
||||
}, {
|
||||
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
|
||||
onPress: onChannelQuickAction,
|
||||
buttonType: 'opacity',
|
||||
}]), [isTablet, searchTerm, onChannelQuickAction]);
|
||||
const rightButtons: HeaderRightButton[] = useMemo(() => ([
|
||||
|
||||
// {
|
||||
// iconName: 'magnify',
|
||||
// onPress: () => {
|
||||
// DeviceEventEmitter.emit(Navigation.NAVIGATE_TO_TAB, {screen: 'Search', params: {searchTerm: `in: ${searchTerm}`}});
|
||||
// if (!isTablet) {
|
||||
// popTopScreen(componentId);
|
||||
// }
|
||||
// },
|
||||
// },
|
||||
{
|
||||
iconName: Platform.select({android: 'dots-vertical', default: 'dots-horizontal'}),
|
||||
onPress: onChannelQuickAction,
|
||||
buttonType: 'opacity',
|
||||
}]), [isTablet, searchTerm, onChannelQuickAction]);
|
||||
|
||||
let title = displayName;
|
||||
if (isOwnDirectMessage) {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|||
|
||||
import CustomStatus from './custom_status';
|
||||
import Logout from './logout';
|
||||
import SavedMessages from './saved_messages';
|
||||
import Settings from './settings';
|
||||
import UserPresence from './user_presence';
|
||||
import YourProfile from './your_profile';
|
||||
|
|
@ -82,11 +81,11 @@ const AccountOptions = ({user, enableCustomUserStatuses, isCustomStatusExpirySup
|
|||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
<SavedMessages
|
||||
{/* <SavedMessages
|
||||
isTablet={isTablet}
|
||||
style={styles.menuLabel}
|
||||
theme={theme}
|
||||
/>
|
||||
/> */}
|
||||
<Settings
|
||||
isTablet={isTablet}
|
||||
style={styles.menuLabel}
|
||||
|
|
|
|||
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, TextStyle} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import MenuItem from '@components/menu_item';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {showModal} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
type Props = {
|
||||
isTablet: boolean;
|
||||
style: TextStyle;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const SavedMessages = ({isTablet, style, theme}: Props) => {
|
||||
const intl = useIntl();
|
||||
const openSavedMessages = useCallback(preventDoubleTap(() => {
|
||||
if (isTablet) {
|
||||
DeviceEventEmitter.emit(Events.ACCOUNT_SELECT_TABLET_VIEW, Screens.SAVED_POSTS);
|
||||
} else {
|
||||
const closeButtonId = 'close-saved-posts';
|
||||
const icon = CompassIcon.getImageSourceSync('close', 24, theme.centerChannelColor);
|
||||
const options = {
|
||||
topBar: {
|
||||
leftButtons: [{
|
||||
id: closeButtonId,
|
||||
testID: closeButtonId,
|
||||
icon,
|
||||
}],
|
||||
},
|
||||
};
|
||||
showModal(
|
||||
Screens.SAVED_POSTS,
|
||||
intl.formatMessage({id: 'mobile.screen.saved_posts', defaultMessage: 'Saved Messages'}),
|
||||
{closeButtonId},
|
||||
options,
|
||||
);
|
||||
}
|
||||
}), [isTablet]);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
testID='account.saved_messages.action'
|
||||
labelComponent={
|
||||
<FormattedText
|
||||
id='account.saved_messages'
|
||||
defaultMessage='Saved Messages'
|
||||
style={style}
|
||||
/>
|
||||
}
|
||||
iconName='bookmark-outline'
|
||||
onPress={openSavedMessages}
|
||||
separator={false}
|
||||
theme={theme}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SavedMessages;
|
||||
|
|
@ -7,7 +7,6 @@ import {DeviceEventEmitter} from 'react-native';
|
|||
import {Events, Screens} from '@constants';
|
||||
import CustomStatus from '@screens/custom_status';
|
||||
import EditProfile from '@screens/edit_profile';
|
||||
import SavedPosts from '@screens/saved_posts';
|
||||
|
||||
type SelectedView = {
|
||||
id: string;
|
||||
|
|
@ -17,7 +16,6 @@ type SelectedView = {
|
|||
const TabletView: Record<string, React.ReactNode> = {
|
||||
[Screens.CUSTOM_STATUS]: CustomStatus,
|
||||
[Screens.EDIT_PROFILE]: EditProfile,
|
||||
[Screens.SAVED_POSTS]: SavedPosts,
|
||||
};
|
||||
|
||||
const AccountTabletView = () => {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import {notificationError} from '@utils/notification';
|
|||
import Account from './account';
|
||||
import ChannelList from './channel_list';
|
||||
import RecentMentions from './recent_mentions';
|
||||
import Search from './search';
|
||||
import SavedMessages from './saved_messages';
|
||||
import TabBar from './tab_bar';
|
||||
|
||||
// import Search from './search';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
|
|
@ -116,16 +118,21 @@ export default function HomeScreen(props: HomeProps) {
|
|||
>
|
||||
{() => <ChannelList {...props}/>}
|
||||
</Tab.Screen>
|
||||
<Tab.Screen
|
||||
{/* <Tab.Screen
|
||||
name={Screens.SEARCH}
|
||||
component={Search}
|
||||
options={{unmountOnBlur: false, lazy: true, tabBarTestID: 'tab_bar.search.tab'}}
|
||||
/>
|
||||
/> */}
|
||||
<Tab.Screen
|
||||
name={Screens.MENTIONS}
|
||||
component={RecentMentions}
|
||||
options={{tabBarTestID: 'tab_bar.mentions.tab', lazy: true, unmountOnBlur: false}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.SAVED_MESSAGES}
|
||||
component={SavedMessages}
|
||||
options={{unmountOnBlur: false, lazy: true, tabBarTestID: 'tab_bar.saved_messages.tab'}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name={Screens.ACCOUNT}
|
||||
component={Account}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import {useIsFocused, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useState, useEffect, useMemo} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {ActivityIndicator, DeviceEventEmitter, FlatList, StyleSheet, View} from 'react-native';
|
||||
import {ActivityIndicator, DeviceEventEmitter, FlatList, Platform, StyleSheet, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {SafeAreaView, Edge, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ 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 {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
|
|
@ -38,6 +39,10 @@ const styles = StyleSheet.create({
|
|||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: Platform.select({ios: BOTTOM_TAB_HEIGHT}),
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
|
|
@ -74,11 +79,13 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
|||
}, [isFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
fetchRecentMentions(serverUrl).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, [serverUrl]);
|
||||
if (isFocused) {
|
||||
setLoading(true);
|
||||
fetchRecentMentions(serverUrl).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [serverUrl, isFocused]);
|
||||
|
||||
const {scrollPaddingTop, scrollRef, scrollValue, onScroll, headerHeight} = useCollapsibleHeader<FlatList<string>>(true, onSnap);
|
||||
const paddingTop = useMemo(() => ({paddingTop: scrollPaddingTop - insets.top, flexGrow: 1}), [scrollPaddingTop, insets.top]);
|
||||
|
|
@ -168,7 +175,7 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
|||
style={styles.flex}
|
||||
edges={EDGES}
|
||||
>
|
||||
<Animated.View style={[styles.flex, animated]}>
|
||||
<Animated.View style={[styles.container, animated]}>
|
||||
<Animated.View style={top}>
|
||||
<RoundedHeaderContext/>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => ({
|
|||
},
|
||||
}));
|
||||
|
||||
function EmptySavedPosts() {
|
||||
function EmptySavedMessages() {
|
||||
const theme = useTheme();
|
||||
const styles = getStyleSheet(theme);
|
||||
|
||||
|
|
@ -44,18 +44,18 @@ function EmptySavedPosts() {
|
|||
<SavedPostsIcon style={styles.icon}/>
|
||||
<FormattedText
|
||||
defaultMessage='No saved messages yet'
|
||||
id='saved_posts.empty.title'
|
||||
id='saved_messages.empty.title'
|
||||
style={styles.title}
|
||||
testID='saved_posts.empty.title'
|
||||
testID='saved_messages.empty.title'
|
||||
/>
|
||||
<FormattedText
|
||||
defaultMessage={'To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.'}
|
||||
id='saved_posts.empty.paragraph'
|
||||
id='saved_messages.empty.paragraph'
|
||||
style={styles.paragraph}
|
||||
testID='saved_posts.empty.paragraph'
|
||||
testID='saved_messages.empty.paragraph'
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export default EmptySavedPosts;
|
||||
export default EmptySavedMessages;
|
||||
207
app/screens/home/saved_messages/saved_posts.tsx
Normal file
207
app/screens/home/saved_messages/saved_posts.tsx
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useIsFocused, useRoute} from '@react-navigation/native';
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {DeviceEventEmitter, FlatList, Platform, StyleSheet, View} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchSavedPosts} from '@actions/remote/post';
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import Loading from '@components/loading';
|
||||
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 {BOTTOM_TAB_HEIGHT} from '@constants/view';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import {isDateLine, getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
import EmptyState from './components/empty';
|
||||
|
||||
import type {ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
currentTimezone: string | null;
|
||||
isTimezoneEnabled: boolean;
|
||||
posts: PostModel[];
|
||||
}
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
marginBottom: Platform.select({ios: BOTTOM_TAB_HEIGHT}),
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
});
|
||||
|
||||
function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
const intl = useIntl();
|
||||
const [loading, setLoading] = useState(!posts.length);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const route = useRoute();
|
||||
const isFocused = useIsFocused();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const params = route.params as {direction: string};
|
||||
const toLeft = params.direction === 'left';
|
||||
const translateSide = toLeft ? -25 : 25;
|
||||
const opacity = useSharedValue(isFocused ? 1 : 0);
|
||||
const translateX = useSharedValue(isFocused ? 0 : translateSide);
|
||||
|
||||
const title = intl.formatMessage({id: 'screen.saved_messages.title', defaultMessage: 'Saved Messages'});
|
||||
const subtitle = intl.formatMessage({id: 'screen.saved_messages.subtitle', defaultMessage: 'All messages you\'ve saved for follow up'});
|
||||
|
||||
const onSnap = (offset: number) => {
|
||||
scrollRef.current?.scrollToOffset({offset, animated: true});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = isFocused ? 1 : 0;
|
||||
translateX.value = isFocused ? 0 : translateSide;
|
||||
}, [isFocused]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused) {
|
||||
setLoading(true);
|
||||
fetchSavedPosts(serverUrl).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
}, [serverUrl, isFocused]);
|
||||
|
||||
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 data = useMemo(() => selectOrderedPosts(posts, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [posts]);
|
||||
|
||||
const animated = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: withTiming(opacity.value, {duration: 150}),
|
||||
transform: [{translateX: withTiming(translateX.value, {duration: 150})}],
|
||||
};
|
||||
}, []);
|
||||
|
||||
const top = useAnimatedStyle(() => {
|
||||
return {
|
||||
top: headerHeight.value,
|
||||
};
|
||||
});
|
||||
|
||||
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
|
||||
if (!viewableItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
|
||||
if (isViewable) {
|
||||
acc[`${Screens.SAVED_MESSAGES}-${item.id}`] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, viewableItemsMap);
|
||||
}, []);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await fetchSavedPosts(serverUrl);
|
||||
setRefreshing(false);
|
||||
}, [serverUrl]);
|
||||
|
||||
const emptyList = useMemo(() => (
|
||||
<View style={styles.empty}>
|
||||
{loading ? (
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
/>
|
||||
) : (
|
||||
<EmptyState/>
|
||||
)}
|
||||
</View>
|
||||
), [loading, theme.buttonBg]);
|
||||
|
||||
const renderItem = useCallback(({item}) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
theme={theme}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
location={Screens.SAVED_MESSAGES}
|
||||
post={item}
|
||||
/>
|
||||
);
|
||||
}, [currentTimezone, isTimezoneEnabled, theme]);
|
||||
|
||||
return (
|
||||
<FreezeScreen freeze={!isFocused}>
|
||||
<NavigationHeader
|
||||
isLargeTitle={true}
|
||||
showBackButton={false}
|
||||
subtitle={subtitle}
|
||||
title={title}
|
||||
hasSearch={false}
|
||||
scrollValue={scrollValue}
|
||||
/>
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
>
|
||||
<Animated.View style={[styles.container, animated]}>
|
||||
<Animated.View style={top}>
|
||||
<RoundedHeaderContext/>
|
||||
</Animated.View>
|
||||
<AnimatedFlatList
|
||||
ref={scrollRef}
|
||||
contentContainerStyle={paddingTop}
|
||||
ListEmptyComponent={emptyList}
|
||||
data={data}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
renderItem={renderItem}
|
||||
scrollToOverflowEnabled={true}
|
||||
showsVerticalScrollIndicator={false}
|
||||
progressViewOffset={scrollPaddingTop}
|
||||
scrollEventThrottle={16}
|
||||
indicatorStyle='black'
|
||||
onScroll={onScroll}
|
||||
removeClippedSubviews={true}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
style={scrollViewStyle}
|
||||
/>
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</FreezeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
export default SavedMessages;
|
||||
|
|
@ -14,6 +14,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
|||
import Account from './account';
|
||||
import Home from './home';
|
||||
import Mentions from './mentions';
|
||||
import SavedMessages from './saved_messages';
|
||||
import Search from './search';
|
||||
|
||||
import type {BottomTabBarProps} from '@react-navigation/bottom-tabs';
|
||||
|
|
@ -55,6 +56,7 @@ const TabComponents: Record<string, any> = {
|
|||
Account,
|
||||
Home,
|
||||
Mentions,
|
||||
SavedMessages,
|
||||
Search,
|
||||
};
|
||||
|
||||
|
|
|
|||
28
app/screens/home/tab_bar/saved_messages.tsx
Normal file
28
app/screens/home/tab_bar/saved_messages.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {BOTTOM_TAB_ICON_SIZE} from '@constants/view';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
isFocused: boolean;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
const SaveMessages = ({isFocused, theme}: Props) => {
|
||||
return (
|
||||
<View>
|
||||
<CompassIcon
|
||||
size={BOTTOM_TAB_ICON_SIZE}
|
||||
name='bookmark-outline'
|
||||
color={isFocused ? theme.buttonBg : changeOpacity(theme.centerChannelColor, 0.48)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SaveMessages;
|
||||
|
|
@ -161,9 +161,6 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
|||
case Screens.REACTIONS:
|
||||
screen = withServerDatabase(require('@screens/reactions').default);
|
||||
break;
|
||||
case Screens.SAVED_POSTS:
|
||||
screen = withServerDatabase((require('@screens/saved_posts').default));
|
||||
break;
|
||||
case Screens.SETTINGS:
|
||||
screen = withServerDatabase(require('@screens/settings').default);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,203 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useMemo, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {BackHandler, DeviceEventEmitter, FlatList, StyleSheet, View} from 'react-native';
|
||||
import {EventSubscription, Navigation} from 'react-native-navigation';
|
||||
import {Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {fetchSavedPosts} from '@actions/remote/post';
|
||||
import Loading from '@components/loading';
|
||||
import DateSeparator from '@components/post_list/date_separator';
|
||||
import PostWithChannelInfo from '@components/post_with_channel_info';
|
||||
import TabletTitle from '@components/tablet_title';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import {isDateLine, getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
import EmptyState from './components/empty';
|
||||
|
||||
import type {ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
componentId?: string;
|
||||
closeButtonId?: string;
|
||||
currentTimezone: string | null;
|
||||
isTimezoneEnabled: boolean;
|
||||
isTablet?: boolean;
|
||||
posts: PostModel[];
|
||||
}
|
||||
|
||||
const edges: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
empty: {
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
list: {
|
||||
paddingVertical: 8,
|
||||
},
|
||||
loading: {
|
||||
height: 40,
|
||||
width: 40,
|
||||
justifyContent: 'center' as const,
|
||||
},
|
||||
});
|
||||
|
||||
function SavedMessages({
|
||||
componentId,
|
||||
closeButtonId,
|
||||
posts,
|
||||
currentTimezone,
|
||||
isTimezoneEnabled,
|
||||
isTablet,
|
||||
}: Props) {
|
||||
const intl = useIntl();
|
||||
const [loading, setLoading] = useState(!posts.length);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
|
||||
const data = useMemo(() => selectOrderedPosts(posts, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [posts]);
|
||||
|
||||
const close = () => {
|
||||
if (componentId) {
|
||||
dismissModal({componentId});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchSavedPosts(serverUrl).finally(() => {
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let unsubscribe: EventSubscription | undefined;
|
||||
if (componentId && closeButtonId) {
|
||||
unsubscribe = Navigation.events().registerComponentListener({
|
||||
navigationButtonPressed: ({buttonId}: { buttonId: string }) => {
|
||||
switch (buttonId) {
|
||||
case closeButtonId:
|
||||
close();
|
||||
break;
|
||||
}
|
||||
},
|
||||
}, componentId);
|
||||
}
|
||||
|
||||
return () => {
|
||||
unsubscribe?.remove();
|
||||
};
|
||||
}, [componentId, closeButtonId]);
|
||||
|
||||
useEffect(() => {
|
||||
let listener: EventSubscription|undefined;
|
||||
if (!isTablet && componentId) {
|
||||
listener = BackHandler.addEventListener('hardwareBackPress', () => {
|
||||
if (EphemeralStore.getNavigationTopComponentId() === componentId) {
|
||||
close();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return () => listener?.remove();
|
||||
}, [componentId, isTablet]);
|
||||
|
||||
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
|
||||
if (!viewableItems.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
|
||||
if (isViewable) {
|
||||
acc[`${Screens.SAVED_POSTS}-${item.id}`] = true;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
DeviceEventEmitter.emit(Events.ITEM_IN_VIEWPORT, viewableItemsMap);
|
||||
}, []);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setRefreshing(true);
|
||||
await fetchSavedPosts(serverUrl);
|
||||
setRefreshing(false);
|
||||
}, [serverUrl]);
|
||||
|
||||
const emptyList = useMemo(() => (
|
||||
<View style={styles.empty}>
|
||||
{loading ? (
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
/>
|
||||
) : (
|
||||
<EmptyState/>
|
||||
)}
|
||||
</View>
|
||||
), [loading, theme.buttonBg]);
|
||||
|
||||
const renderItem = useCallback(({item}) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
theme={theme}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
location={Screens.SAVED_POSTS}
|
||||
post={item}
|
||||
/>
|
||||
);
|
||||
}, [currentTimezone, isTimezoneEnabled, theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isTablet &&
|
||||
<TabletTitle
|
||||
testID='custom_status.done.button'
|
||||
title={intl.formatMessage({id: 'mobile.screen.saved_posts', defaultMessage: 'Saved Messages'})}
|
||||
/>
|
||||
}
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={data.length ? styles.list : [styles.empty]}
|
||||
ListEmptyComponent={emptyList}
|
||||
data={data}
|
||||
onRefresh={handleRefresh}
|
||||
refreshing={refreshing}
|
||||
renderItem={renderItem}
|
||||
scrollToOverflowEnabled={true}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SavedMessages;
|
||||
|
|
@ -104,7 +104,7 @@ const SnackBar = ({barType, componentId, onAction, sourceScreen}: SnackBarProps)
|
|||
marginBottom: 30,
|
||||
};
|
||||
break;
|
||||
case sourceScreen === Screens.SAVED_POSTS :
|
||||
case sourceScreen === Screens.SAVED_MESSAGES :
|
||||
tabletStyle = {
|
||||
marginBottom: 20,
|
||||
marginLeft: TABLET_SIDEBAR_WIDTH,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
"about.teamEditiont1": "Enterprise Edition",
|
||||
"account.logout": "Log out",
|
||||
"account.logout_from": "Log out of {serverName}",
|
||||
"account.saved_messages": "Saved Messages",
|
||||
"account.settings": "Settings",
|
||||
"account.user_status.title": "User Presence",
|
||||
"account.your_profile": "Your Profile",
|
||||
|
|
@ -129,16 +128,22 @@
|
|||
"channel_info.leave_channel": "Leave channel",
|
||||
"channel_info.leave_private_channel": "Are you sure you want to leave the private channel {displayName}? You cannot rejoin the channel unless you're invited again.",
|
||||
"channel_info.leave_public_channel": "Are you sure you want to leave the public channel {displayName}? You can always rejoin.",
|
||||
"channel_info.local_time": "Local Time",
|
||||
"channel_info.members": "Members",
|
||||
"channel_info.mention": "Mention",
|
||||
"channel_info.mobile_notifications": "Mobile Notifications",
|
||||
"channel_info.muted": "Mute",
|
||||
"channel_info.nickname": "Nickname",
|
||||
"channel_info.notification.all": "All",
|
||||
"channel_info.notification.default": "Default",
|
||||
"channel_info.notification.mention": "Mentions",
|
||||
"channel_info.notification.none": "Never",
|
||||
"channel_info.pinned_messages": "Pinned Messages",
|
||||
"channel_info.position": "Position",
|
||||
"channel_info.private_channel": "Private Channel",
|
||||
"channel_info.public_channel": "Public Channel",
|
||||
"channel_info.send_a_mesasge": "Send a message",
|
||||
"channel_info.send_mesasge": "Send message",
|
||||
"channel_info.set_header": "Set Header",
|
||||
"channel_info.unarchive": "Unarchive Channel",
|
||||
"channel_info.unarchive_description": "Are you sure you want to unarchive the {term} {name}?",
|
||||
|
|
@ -498,7 +503,6 @@
|
|||
"mobile.routes.custom_status": "Set a Status",
|
||||
"mobile.routes.table": "Table",
|
||||
"mobile.routes.user_profile": "Profile",
|
||||
"mobile.screen.saved_posts": "Saved Messages",
|
||||
"mobile.screen.settings": "Settings",
|
||||
"mobile.screen.your_profile": "Your Profile",
|
||||
"mobile.search.jump": "Jump to recent messages",
|
||||
|
|
@ -621,10 +625,12 @@
|
|||
"post.reactions.title": "Reactions",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
"public_link_copied": "Link copied to clipboard",
|
||||
"saved_posts.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
||||
"saved_posts.empty.title": "No saved messages yet",
|
||||
"saved_messages.empty.paragraph": "To save something for later, long-press on a message and choose Save from the menu. Saved messages are only visible to you.",
|
||||
"saved_messages.empty.title": "No saved messages yet",
|
||||
"screen.mentions.subtitle": "Messages you've been mentioned in",
|
||||
"screen.mentions.title": "Recent Mentions",
|
||||
"screen.saved_messages.subtitle": "All messages you've saved for follow up",
|
||||
"screen.saved_messages.title": "Saved Messages",
|
||||
"screen.search.header.files": "Files",
|
||||
"screen.search.header.messages": "Messages",
|
||||
"screen.search.placeholder": "Search messages & files",
|
||||
|
|
@ -712,6 +718,7 @@
|
|||
"user.settings.general.nickname": "Nickname",
|
||||
"user.settings.general.position": "Position",
|
||||
"user.settings.general.username": "Username",
|
||||
"user.tutorial.long_press": "Long-press on an item to view a user's profile",
|
||||
"video.download": "Download video",
|
||||
"video.failed_description": "An error occurred while trying to play the video.\n",
|
||||
"video.failed_title": "Video playback failed",
|
||||
|
|
|
|||
Loading…
Reference in a new issue