forked from Ivasoft/mattermost-mobile
* Add more messages button * Update existing tests * Various fixes: * Use viewAreaCoveragePercentThreshold over itemVisiblePercentThreshold * Remove currentUserId check when adding New Message line to postIds and instead update the channel last viewed time when an own post is received * Update snapshots * Add showMoreMessagesButton prop and default to false * Android fixes * Add tests * Localize more messages text * Use FormattedText * i18 extract * Style fixes * Account for network indicator * Fix for failing tests * Various fixes: * Set the unreadMessageCount when POST_UNREAD_SUCCESS is dispatched with a positive deltaMsgs * Hide the more messages button when the unread count decreases or when the new message line is removed shortly after loading the channel * No need for POST_UNREAD_SUCCESS if we manually call onViewableItemsChanged * Reset unread count if current channel on channel mount * Animate text opacity * Compare indeces to determine when scrolling has ended * Fix opacity animation trigger * try with scrolling to the last rendered item * Add onScrollEndIndex * Improve animations * Don't track moreCount in state * Use moreText over prevNewMessageLineIndex to determine firstPage * Update intl message format and call cancel in componentDidUpdate when needed * Fix intl format * Remove opacity animation and countText * Fix pressed not being reset * No need to separate intl func * Return after resetting * Fix accidental removal of setState call * Reset pressed when newLineMessageIndex changes * Use default windowSize and lower POST_CHUNK_SIZE and delays * Queue a cancel timer that gets cleared only when the newMessageLineIndex changes * Define uncancel func * Increase cancelTimer delay * Subtract read posts from unread count and account for retry indicator * Add retry bar indicator tests * Use props.unreadCount * Fix handling of newMessageLineIndex change * Fix handling of newMessageLineIndex change take 2 * Fix handling of newMessageLineIndex change take 3 * Use 'native' TouchableWithFeedback with dark overlay * Fix handling of manually unread * Update chunk and window sizes * Fix hsl * Update text only when newMessageLineIndex/endIndex is reached * Don't delay cancel if when no more unreads * Fixes for when opening the app * No need to process viewableItems when unreadCount is 0 * Remove line * Don't show if unreadCount is 0 Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
606 lines
20 KiB
JavaScript
606 lines
20 KiB
JavaScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import React, {PureComponent} from 'react';
|
|
import PropTypes from 'prop-types';
|
|
import {Alert, FlatList, Platform, RefreshControl, StyleSheet} from 'react-native';
|
|
import {intlShape} from 'react-intl';
|
|
|
|
import {Posts} from '@mm-redux/constants';
|
|
import EventEmitter from '@mm-redux/utils/event_emitter';
|
|
import * as PostListUtils from '@mm-redux/utils/post_list';
|
|
|
|
import CombinedUserActivityPost from 'app/components/combined_user_activity_post';
|
|
import Post from 'app/components/post';
|
|
import {DeepLinkTypes, ListTypes} from 'app/constants';
|
|
import mattermostManaged from 'app/mattermost_managed';
|
|
import {makeExtraData} from 'app/utils/list_view';
|
|
import {changeOpacity} from 'app/utils/theme';
|
|
import {matchDeepLink} from 'app/utils/url';
|
|
import telemetry from 'app/telemetry';
|
|
import {showModalOverCurrentContext} from 'app/actions/navigation';
|
|
import {alertErrorWithFallback} from 'app/utils/general';
|
|
import {t} from 'app/utils/i18n';
|
|
|
|
import DateHeader from './date_header';
|
|
import NewMessagesDivider from './new_messages_divider';
|
|
import MoreMessagesButton from './more_messages_button';
|
|
|
|
const INITIAL_BATCH_TO_RENDER = 10;
|
|
const SCROLL_UP_MULTIPLIER = 3.5;
|
|
const SCROLL_POSITION_CONFIG = {
|
|
|
|
// To avoid scrolling the list when new messages arrives
|
|
// if the user is not at the bottom
|
|
minIndexForVisible: 0,
|
|
|
|
// If the user is at the bottom or 60px from the bottom
|
|
// auto scroll show the new message
|
|
autoscrollToTopThreshold: 60,
|
|
};
|
|
|
|
export default class PostList extends PureComponent {
|
|
static propTypes = {
|
|
actions: PropTypes.shape({
|
|
handleSelectChannelByName: PropTypes.func.isRequired,
|
|
loadChannelsByTeamName: PropTypes.func.isRequired,
|
|
refreshChannelWithRetry: PropTypes.func.isRequired,
|
|
selectFocusedPostId: PropTypes.func.isRequired,
|
|
setDeepLinkURL: PropTypes.func.isRequired,
|
|
}).isRequired,
|
|
channelId: PropTypes.string,
|
|
deepLinkURL: PropTypes.string,
|
|
extraData: PropTypes.any,
|
|
highlightPinnedOrFlagged: PropTypes.bool,
|
|
highlightPostId: PropTypes.string,
|
|
initialIndex: PropTypes.number,
|
|
isSearchResult: PropTypes.bool,
|
|
lastPostIndex: PropTypes.number.isRequired,
|
|
lastViewedAt: PropTypes.number, // Used by container // eslint-disable-line no-unused-prop-types
|
|
loadMorePostsVisible: PropTypes.bool,
|
|
onLoadMoreUp: PropTypes.func,
|
|
onHashtagPress: PropTypes.func,
|
|
onPermalinkPress: PropTypes.func,
|
|
onPostPress: PropTypes.func,
|
|
onRefresh: PropTypes.func,
|
|
postIds: PropTypes.array.isRequired,
|
|
refreshing: PropTypes.bool,
|
|
renderFooter: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
|
renderReplies: PropTypes.bool,
|
|
serverURL: PropTypes.string.isRequired,
|
|
shouldRenderReplyButton: PropTypes.bool,
|
|
siteURL: PropTypes.string.isRequired,
|
|
theme: PropTypes.object.isRequired,
|
|
location: PropTypes.string,
|
|
scrollViewNativeID: PropTypes.string,
|
|
showMoreMessagesButton: PropTypes.bool,
|
|
};
|
|
|
|
static defaultProps = {
|
|
onLoadMoreUp: () => true,
|
|
renderFooter: () => null,
|
|
refreshing: false,
|
|
serverURL: '',
|
|
siteURL: '',
|
|
postIds: [],
|
|
showMoreMessagesButton: false,
|
|
};
|
|
|
|
static contextTypes = {
|
|
intl: intlShape.isRequired,
|
|
};
|
|
|
|
constructor(props) {
|
|
super(props);
|
|
|
|
this.contentOffsetY = 0;
|
|
this.contentHeight = 0;
|
|
this.hasDoneInitialScroll = false;
|
|
this.shouldScrollToBottom = false;
|
|
this.makeExtraData = makeExtraData();
|
|
this.flatListRef = React.createRef();
|
|
}
|
|
|
|
componentDidMount() {
|
|
const {actions, deepLinkURL, highlightPostId, initialIndex} = this.props;
|
|
|
|
EventEmitter.on('scroll-to-bottom', this.handleSetScrollToBottom);
|
|
|
|
// Invoked when hitting a deep link and app is not already running.
|
|
if (deepLinkURL) {
|
|
this.handleDeepLink(deepLinkURL);
|
|
actions.setDeepLinkURL('');
|
|
}
|
|
|
|
// Scroll to highlighted post for permalinks
|
|
if (!this.hasDoneInitialScroll && initialIndex > 0 && highlightPostId) {
|
|
this.scrollToInitialIndexIfNeeded(initialIndex);
|
|
}
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const {actions, channelId, deepLinkURL, postIds} = this.props;
|
|
|
|
if (this.props.channelId !== prevProps.channelId) {
|
|
this.resetPostList();
|
|
}
|
|
|
|
// Invoked when hitting a deep link and app is already running.
|
|
if (deepLinkURL && deepLinkURL !== prevProps.deepLinkURL) {
|
|
this.handleDeepLink(deepLinkURL);
|
|
actions.setDeepLinkURL('');
|
|
}
|
|
|
|
if (this.shouldScrollToBottom && prevProps.channelId === channelId && prevProps.postIds.length === postIds.length) {
|
|
this.scrollToBottom();
|
|
this.shouldScrollToBottom = false;
|
|
}
|
|
|
|
if (
|
|
this.props.channelId === prevProps.channelId &&
|
|
this.props.postIds.length &&
|
|
this.contentHeight &&
|
|
this.contentHeight < this.postListHeight &&
|
|
this.props.extraData
|
|
) {
|
|
this.loadToFillContent();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
EventEmitter.off('scroll-to-bottom', this.handleSetScrollToBottom);
|
|
|
|
this.resetPostList();
|
|
}
|
|
|
|
flatListScrollToIndex = (index) => {
|
|
this.animationFrameInitialIndex = requestAnimationFrame(() => {
|
|
this.flatListRef.current.scrollToIndex({
|
|
animated: true,
|
|
index,
|
|
viewOffset: 0,
|
|
viewPosition: 1, // 0 is at bottom
|
|
});
|
|
});
|
|
}
|
|
|
|
getItemCount = () => {
|
|
return this.props.postIds.length;
|
|
};
|
|
|
|
handleClosePermalink = () => {
|
|
const {actions} = this.props;
|
|
actions.selectFocusedPostId('');
|
|
this.showingPermalink = false;
|
|
};
|
|
|
|
handleContentSizeChange = (contentWidth, contentHeight, forceLoad) => {
|
|
this.contentHeight = contentHeight;
|
|
const loadMore = forceLoad || !this.props.extraData;
|
|
if (this.postListHeight && contentHeight < this.postListHeight && loadMore) {
|
|
// We still have less than 1 screen of posts loaded with more to get, so load more
|
|
this.props.onLoadMoreUp();
|
|
}
|
|
};
|
|
|
|
handleDeepLink = (url) => {
|
|
const {serverURL, siteURL} = this.props;
|
|
|
|
const match = matchDeepLink(url, serverURL, siteURL);
|
|
|
|
if (match) {
|
|
if (match.type === DeepLinkTypes.CHANNEL) {
|
|
this.props.actions.handleSelectChannelByName(match.channelName, match.teamName, this.permalinkBadChannel);
|
|
} else if (match.type === DeepLinkTypes.PERMALINK) {
|
|
this.handlePermalinkPress(match.postId, match.teamName);
|
|
}
|
|
} else {
|
|
const {formatMessage} = this.context.intl;
|
|
Alert.alert(
|
|
formatMessage({
|
|
id: 'mobile.server_link.error.title',
|
|
defaultMessage: 'Link Error',
|
|
}),
|
|
formatMessage({
|
|
id: 'mobile.server_link.error.text',
|
|
defaultMessage: 'The link could not be found on this server.',
|
|
}),
|
|
);
|
|
}
|
|
};
|
|
|
|
handleLayout = (event) => {
|
|
const {height} = event.nativeEvent.layout;
|
|
if (this.postListHeight !== height) {
|
|
this.postListHeight = height;
|
|
}
|
|
};
|
|
|
|
handlePermalinkPress = (postId, teamName) => {
|
|
telemetry.start(['post_list:permalink']);
|
|
const {actions, onPermalinkPress} = this.props;
|
|
|
|
if (onPermalinkPress) {
|
|
onPermalinkPress(postId, true);
|
|
} else {
|
|
actions.loadChannelsByTeamName(teamName, this.permalinkBadTeam);
|
|
this.showPermalinkView(postId);
|
|
}
|
|
};
|
|
|
|
handleRefresh = () => {
|
|
const {
|
|
actions,
|
|
channelId,
|
|
onRefresh,
|
|
} = this.props;
|
|
|
|
if (channelId) {
|
|
actions.refreshChannelWithRetry(channelId);
|
|
}
|
|
|
|
if (onRefresh) {
|
|
onRefresh();
|
|
}
|
|
};
|
|
|
|
handleScroll = (event) => {
|
|
const pageOffsetY = event.nativeEvent.contentOffset.y;
|
|
if (pageOffsetY > 0) {
|
|
const contentHeight = event.nativeEvent.contentSize.height;
|
|
const direction = (this.contentOffsetY < pageOffsetY) ? ListTypes.VISIBILITY_SCROLL_UP : ListTypes.VISIBILITY_SCROLL_DOWN;
|
|
|
|
this.contentOffsetY = pageOffsetY;
|
|
if (
|
|
direction === ListTypes.VISIBILITY_SCROLL_UP &&
|
|
(contentHeight - pageOffsetY) < (this.postListHeight * SCROLL_UP_MULTIPLIER)
|
|
) {
|
|
this.props.onLoadMoreUp();
|
|
}
|
|
}
|
|
};
|
|
|
|
handleScrollToIndexFailed = (info) => {
|
|
this.animationFrameIndexFailed = requestAnimationFrame(() => {
|
|
if (this.onScrollEndIndexListener) {
|
|
this.onScrollEndIndexListener(info.highestMeasuredFrameIndex);
|
|
}
|
|
this.flatListScrollToIndex(info.highestMeasuredFrameIndex);
|
|
});
|
|
};
|
|
|
|
handleSetScrollToBottom = () => {
|
|
this.shouldScrollToBottom = true;
|
|
}
|
|
|
|
keyExtractor = (item) => {
|
|
// All keys are strings (either post IDs or special keys)
|
|
return item;
|
|
};
|
|
|
|
loadToFillContent = () => {
|
|
this.fillContentTimer = setTimeout(() => {
|
|
this.handleContentSizeChange(0, this.contentHeight, true);
|
|
});
|
|
};
|
|
|
|
permalinkBadTeam = () => {
|
|
const {intl} = this.context;
|
|
const message = {
|
|
id: t('mobile.server_link.unreachable_team.error'),
|
|
defaultMessage: 'This link belongs to a deleted team or to a team to which you do not have access.',
|
|
};
|
|
|
|
alertErrorWithFallback(intl, {}, message);
|
|
};
|
|
|
|
permalinkBadChannel = () => {
|
|
const {intl} = this.context;
|
|
const message = {
|
|
id: t('mobile.server_link.unreachable_channel.error'),
|
|
defaultMessage: 'This link belongs to a deleted channel or to a channel to which you do not have access.',
|
|
};
|
|
|
|
alertErrorWithFallback(intl, {}, message);
|
|
};
|
|
|
|
renderItem = ({item, index}) => {
|
|
const {
|
|
highlightPinnedOrFlagged,
|
|
highlightPostId,
|
|
isSearchResult,
|
|
lastPostIndex,
|
|
location,
|
|
onHashtagPress,
|
|
onPostPress,
|
|
postIds,
|
|
renderReplies,
|
|
shouldRenderReplyButton,
|
|
theme,
|
|
} = this.props;
|
|
|
|
if (PostListUtils.isStartOfNewMessages(item)) {
|
|
// postIds includes a date item after the new message indicator so 2
|
|
// needs to be added to the index for the length check to be correct.
|
|
const moreNewMessages = postIds.length === index + 2;
|
|
|
|
// The date line and new message line each count for a line. So the
|
|
// goal of this is to check for the 3rd previous, which for the start
|
|
// of a thread would be null as it doesn't exist.
|
|
const checkForPostId = index < postIds.length - 3;
|
|
|
|
return (
|
|
<NewMessagesDivider
|
|
index={index}
|
|
theme={theme}
|
|
moreMessages={moreNewMessages && checkForPostId}
|
|
/>
|
|
);
|
|
} else if (PostListUtils.isDateLine(item)) {
|
|
return (
|
|
<DateHeader
|
|
date={PostListUtils.getDateForDateLine(item)}
|
|
index={index}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Remember that the list is rendered with item 0 at the bottom so the "previous" post
|
|
// comes after this one in the list
|
|
const previousPostId = index < postIds.length - 1 ? postIds[index + 1] : null;
|
|
const beforePrevPostId = index < postIds.length - 2 ? postIds[index + 2] : null;
|
|
const nextPostId = index > 0 ? postIds[index - 1] : null;
|
|
|
|
const postProps = {
|
|
previousPostId,
|
|
nextPostId,
|
|
highlightPinnedOrFlagged,
|
|
isSearchResult,
|
|
location,
|
|
managedConfig: mattermostManaged.getCachedConfig(),
|
|
onHashtagPress,
|
|
onPermalinkPress: this.handlePermalinkPress,
|
|
onPress: onPostPress,
|
|
renderReplies,
|
|
shouldRenderReplyButton,
|
|
beforePrevPostId,
|
|
};
|
|
|
|
if (PostListUtils.isCombinedUserActivityPost(item)) {
|
|
return (
|
|
<CombinedUserActivityPost
|
|
combinedId={item}
|
|
{...postProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const postId = item;
|
|
return (
|
|
<MemoizedPost
|
|
postId={postId}
|
|
highlight={highlightPostId === postId}
|
|
isLastPost={lastPostIndex === index}
|
|
{...postProps}
|
|
/>
|
|
);
|
|
};
|
|
|
|
resetPostList = () => {
|
|
this.contentOffsetY = 0;
|
|
this.hasDoneInitialScroll = false;
|
|
|
|
if (this.scrollAfterInteraction) {
|
|
this.scrollAfterInteraction.cancel();
|
|
}
|
|
|
|
if (this.animationFrameIndexFailed) {
|
|
cancelAnimationFrame(this.animationFrameIndexFailed);
|
|
}
|
|
|
|
if (this.animationFrameInitialIndex) {
|
|
cancelAnimationFrame(this.animationFrameInitialIndex);
|
|
}
|
|
|
|
if (this.fillContentTimer) {
|
|
clearTimeout(this.fillContentTimer);
|
|
}
|
|
|
|
if (this.scrollToBottomTimer) {
|
|
clearTimeout(this.scrollToBottomTimer);
|
|
}
|
|
|
|
if (this.scrollToInitialTimer) {
|
|
clearTimeout(this.scrollToInitialTimer);
|
|
}
|
|
|
|
if (this.contentHeight !== 0) {
|
|
this.contentHeight = 0;
|
|
}
|
|
}
|
|
|
|
scrollToBottom = () => {
|
|
this.scrollToBottomTimer = setTimeout(() => {
|
|
if (this.flatListRef.current) {
|
|
this.flatListRef.current.scrollToOffset({offset: 0, animated: true});
|
|
}
|
|
}, 250);
|
|
};
|
|
|
|
scrollToIndex = (index) => {
|
|
this.animationFrameInitialIndex = requestAnimationFrame(() => {
|
|
if (this.flatListRef.current && index > 0 && index <= this.getItemCount()) {
|
|
this.flatListScrollToIndex(index);
|
|
}
|
|
});
|
|
};
|
|
|
|
scrollToInitialIndexIfNeeded = (index) => {
|
|
if (!this.hasDoneInitialScroll) {
|
|
if (index > 0 && index <= this.getItemCount()) {
|
|
this.hasDoneInitialScroll = true;
|
|
this.scrollToIndex(index);
|
|
if (index !== this.props.initialIndex) {
|
|
this.hasDoneInitialScroll = false;
|
|
this.scrollToInitialTimer = setTimeout(() => {
|
|
this.scrollToInitialIndexIfNeeded(this.props.initialIndex);
|
|
}, 10);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
showPermalinkView = (postId, error = '') => {
|
|
const {actions} = this.props;
|
|
|
|
actions.selectFocusedPostId(postId);
|
|
|
|
if (!this.showingPermalink) {
|
|
const screen = 'Permalink';
|
|
const passProps = {
|
|
isPermalink: true,
|
|
onClose: this.handleClosePermalink,
|
|
error,
|
|
};
|
|
const options = {
|
|
layout: {
|
|
componentBackgroundColor: changeOpacity('#000', 0.2),
|
|
},
|
|
};
|
|
|
|
this.showingPermalink = true;
|
|
showModalOverCurrentContext(screen, passProps, options);
|
|
}
|
|
};
|
|
|
|
registerViewableItemsListener = (listener) => {
|
|
this.onViewableItemsChangedListener = listener;
|
|
const removeListener = () => {
|
|
this.onViewableItemsChangedListener = null;
|
|
};
|
|
|
|
return removeListener;
|
|
}
|
|
|
|
registerScrollEndIndexListener = (listener) => {
|
|
this.onScrollEndIndexListener = listener;
|
|
const removeListener = () => {
|
|
this.onScrollEndIndexListener = null;
|
|
};
|
|
|
|
return removeListener;
|
|
}
|
|
|
|
onViewableItemsChanged = ({viewableItems}) => {
|
|
if (!this.onViewableItemsChangedListener || !viewableItems.length || this.props.deepLinkURL) {
|
|
return;
|
|
}
|
|
|
|
this.onViewableItemsChangedListener(viewableItems);
|
|
}
|
|
|
|
render() {
|
|
const {
|
|
channelId,
|
|
extraData,
|
|
highlightPostId,
|
|
loadMorePostsVisible,
|
|
postIds,
|
|
refreshing,
|
|
scrollViewNativeID,
|
|
theme,
|
|
initialIndex,
|
|
deepLinkURL,
|
|
showMoreMessagesButton,
|
|
} = this.props;
|
|
|
|
const refreshControl = (
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={channelId ? this.handleRefresh : null}
|
|
colors={[theme.centerChannelColor]}
|
|
tintColor={theme.centerChannelColor}
|
|
/>);
|
|
|
|
const hasPostsKey = postIds.length ? 'true' : 'false';
|
|
|
|
return (
|
|
<>
|
|
<FlatList
|
|
contentContainerStyle={styles.postListContent}
|
|
data={postIds}
|
|
extraData={this.makeExtraData(channelId, highlightPostId, extraData, loadMorePostsVisible)}
|
|
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
|
inverted={true}
|
|
key={`recyclerFor-${channelId}-${hasPostsKey}`}
|
|
keyboardDismissMode={'interactive'}
|
|
keyboardShouldPersistTaps={'handled'}
|
|
keyExtractor={this.keyExtractor}
|
|
ListFooterComponent={this.props.renderFooter}
|
|
listKey={`recyclerFor-${channelId}`}
|
|
maintainVisibleContentPosition={SCROLL_POSITION_CONFIG}
|
|
maxToRenderPerBatch={Platform.select({android: 5})}
|
|
nativeID={scrollViewNativeID}
|
|
onContentSizeChange={this.handleContentSizeChange}
|
|
onLayout={this.handleLayout}
|
|
onScroll={this.handleScroll}
|
|
onScrollToIndexFailed={this.handleScrollToIndexFailed}
|
|
ref={this.flatListRef}
|
|
refreshControl={refreshControl}
|
|
removeClippedSubviews={false}
|
|
renderItem={this.renderItem}
|
|
scrollEventThrottle={60}
|
|
style={styles.flex}
|
|
windowSize={Posts.POST_CHUNK_SIZE / 2}
|
|
viewabilityConfig={{
|
|
itemVisiblePercentThreshold: 1,
|
|
minimumViewTime: 100,
|
|
}}
|
|
onViewableItemsChanged={showMoreMessagesButton ? this.onViewableItemsChanged : null}
|
|
/>
|
|
{showMoreMessagesButton &&
|
|
<MoreMessagesButton
|
|
theme={theme}
|
|
postIds={postIds}
|
|
channelId={channelId}
|
|
deepLinkURL={deepLinkURL}
|
|
newMessageLineIndex={initialIndex}
|
|
scrollToIndex={this.scrollToIndex}
|
|
registerViewableItemsListener={this.registerViewableItemsListener}
|
|
registerScrollEndIndexListener={this.registerScrollEndIndexListener}
|
|
/>
|
|
}
|
|
</>
|
|
);
|
|
}
|
|
}
|
|
|
|
function PostComponent({postId, highlightPostId, lastPostIndex, index, ...postProps}) {
|
|
return (
|
|
<Post
|
|
postId={postId}
|
|
highlight={highlightPostId === postId}
|
|
isLastPost={lastPostIndex === index}
|
|
{...postProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
PostComponent.propTypes = {
|
|
postId: PropTypes.string.isRequired,
|
|
highlightPostId: PropTypes.string,
|
|
lastPostIndex: PropTypes.number,
|
|
index: PropTypes.number,
|
|
};
|
|
|
|
const MemoizedPost = React.memo(PostComponent);
|
|
|
|
const styles = StyleSheet.create({
|
|
flex: {
|
|
flex: 1,
|
|
},
|
|
postListContent: {
|
|
paddingTop: 5,
|
|
},
|
|
});
|