Files
mattermost-mobile/app/components/post_list/post_list.tsx
Elias Nahum 7e6248dfb3 [Gekidou] - Channel Intro (#5846)
* Channel Intro

* Move avatar margins to post component per feedback review

* Channel intro redesign

* Fix preferences unit test

* Change group intro sizes

* Add Bot tag to DM Intro if they have it

* fix channel isTablet layout on split screen

* update snapshot
2021-12-21 17:44:00 +02:00

234 lines
7.4 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ReactElement, useCallback, useEffect, useRef} from 'react';
import {DeviceEventEmitter, FlatList, Platform, RefreshControl, StyleProp, StyleSheet, ViewStyle, ViewToken} from 'react-native';
import CombinedUserActivity from '@components/post_list/combined_user_activity';
import DateSeparator from '@components/post_list/date_separator';
import NewMessagesLine from '@components/post_list/new_message_line';
import Post from '@components/post_list/post';
import {useTheme} from '@context/theme';
import {emptyFunction} from '@utils/general';
import {getDateForDateLine, isCombinedUserActivityPost, isDateLine, isStartOfNewMessages, preparePostList} from '@utils/post_list';
import type PostModel from '@typings/database/models/servers/post';
type RefreshProps = {
children: ReactElement;
enabled: boolean;
onRefresh: () => void;
refreshing: boolean;
}
type Props = {
channelId: string;
contentContainerStyle?: StyleProp<ViewStyle>;
currentTimezone: string | null;
currentUsername: string;
isTimezoneEnabled: boolean;
lastViewedAt: number;
posts: PostModel[];
shouldShowJoinLeaveMessages: boolean;
footer?: ReactElement;
testID: string;
}
type ViewableItemsChanged = {
viewableItems: ViewToken[];
changed: ViewToken[];
}
const style = StyleSheet.create({
container: {
flex: 1,
scaleY: -1,
},
scale: {
...Platform.select({
android: {
scaleY: -1,
},
}),
},
});
export const VIEWABILITY_CONFIG = {
itemVisiblePercentThreshold: 1,
minimumViewTime: 100,
};
const keyExtractor = (item: string | PostModel) => (typeof item === 'string' ? item : item.id);
const styles = StyleSheet.create({
flex: {
flex: 1,
},
content: {
marginHorizontal: 20,
},
});
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing}: RefreshProps) => {
const props = {
onRefresh,
refreshing,
};
if (Platform.OS === 'android') {
return (
<RefreshControl
{...props}
enabled={enabled}
style={style.container}
>
{children}
</RefreshControl>
);
}
const refreshControl = <RefreshControl {...props}/>;
return React.cloneElement(
children,
{refreshControl, inverted: true},
);
};
const PostList = ({channelId, contentContainerStyle, currentTimezone, currentUsername, footer, isTimezoneEnabled, lastViewedAt, posts, shouldShowJoinLeaveMessages, testID}: Props) => {
const listRef = useRef<FlatList>(null);
const theme = useTheme();
const orderedPosts = preparePostList(posts, lastViewedAt, true, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, currentTimezone, false);
useEffect(() => {
listRef.current?.scrollToOffset({offset: 0, animated: false});
}, [channelId, listRef.current]);
const onViewableItemsChanged = useCallback(({viewableItems}: ViewableItemsChanged) => {
if (!viewableItems.length) {
return;
}
const viewableItemsMap = viewableItems.reduce((acc: Record<string, boolean>, {item, isViewable}) => {
if (isViewable) {
acc[item.id] = true;
}
return acc;
}, {});
DeviceEventEmitter.emit('scrolled', viewableItemsMap);
}, []);
const renderItem = useCallback(({item, index}) => {
if (typeof item === 'string') {
if (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 = orderedPosts.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 < orderedPosts.length - 3;
return (
<NewMessagesLine
theme={theme}
moreMessages={moreNewMessages && checkForPostId}
testID={`${testID}.new_messages_line`}
style={style.scale}
/>
);
} else if (isDateLine(item)) {
return (
<DateSeparator
date={getDateForDateLine(item)}
theme={theme}
style={style.scale}
timezone={isTimezoneEnabled ? currentTimezone : null}
/>
);
}
if (isCombinedUserActivityPost(item)) {
const postProps = {
currentUsername,
postId: item,
style: Platform.OS === 'ios' ? style.scale : style.container,
testID: `${testID}.combined_user_activity`,
showJoinLeave: shouldShowJoinLeaveMessages,
theme,
};
return (<CombinedUserActivity {...postProps}/>);
}
}
let previousPost: PostModel|undefined;
let nextPost: PostModel|undefined;
if (index < posts.length - 1) {
const prev = orderedPosts.slice(index + 1).find((v) => typeof v !== 'string');
if (prev) {
previousPost = prev as PostModel;
}
}
if (index > 0) {
const next = orderedPosts.slice(0, index);
for (let i = next.length - 1; i >= 0; i--) {
const v = next[i];
if (typeof v !== 'string') {
nextPost = v;
break;
}
}
}
const postProps = {
highlightPinnedOrFlagged: true,
location: 'Channel',
nextPost,
previousPost,
shouldRenderReplyButton: true,
};
return (
<Post
key={item.id}
post={item}
style={style.scale}
testID={`${testID}.post`}
{...postProps}
/>
);
}, [orderedPosts, theme]);
return (
<PostListRefreshControl
enabled={false}
refreshing={false}
onRefresh={emptyFunction}
>
<FlatList
contentContainerStyle={[styles.content, contentContainerStyle]}
data={orderedPosts}
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='handled'
keyExtractor={keyExtractor}
initialNumToRender={10}
ListFooterComponent={footer}
maxToRenderPerBatch={10}
onViewableItemsChanged={onViewableItemsChanged}
ref={listRef}
renderItem={renderItem}
removeClippedSubviews={true}
scrollEventThrottle={60}
style={styles.flex}
viewabilityConfig={VIEWABILITY_CONFIG}
/>
</PostListRefreshControl>
);
};
export default PostList;