forked from Ivasoft/mattermost-mobile
Reduce the amount of queries to display the PostList (#6927)
This commit is contained in:
@@ -7,22 +7,32 @@ import React from 'react';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observeSavedPostsByIds} from '@queries/servers/post';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import PostList from './post_list';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const enhanced = withObservables(['posts'], ({database, posts}: {posts: PostModel[]} & WithDatabaseArgs) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
const postIds = posts.map((p) => p.id);
|
||||
|
||||
return {
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone || null))))),
|
||||
currentUserId: currentUser.pipe((switchMap((user) => of$(user?.id)))),
|
||||
currentUsername: currentUser.pipe((switchMap((user) => of$(user?.username)))),
|
||||
savedPostIds: observeSavedPostsByIds(database, postIds),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -17,14 +17,14 @@ import {useIsTablet} from '@hooks/device';
|
||||
import {makeStyleSheetFromTheme, hexToHue} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {PostList} from '@typings/components/post_list';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
isCRTEnabled?: boolean;
|
||||
isManualUnread?: boolean;
|
||||
newMessageLineIndex: number;
|
||||
posts: Array<string | PostModel>;
|
||||
posts: PostList;
|
||||
registerScrollEndIndexListener: (fn: (endIndex: number) => void) => () => void;
|
||||
registerViewableItemsListener: (fn: (viewableItems: ViewToken[]) => void) => () => void;
|
||||
rootId?: string;
|
||||
@@ -188,7 +188,7 @@ const MoreMessages = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const readCount = posts.slice(0, lastViewableIndex).filter((v) => typeof v !== 'string').length;
|
||||
const readCount = posts.slice(0, lastViewableIndex).filter((v) => v.type === 'post').length;
|
||||
const totalUnread = localUnreadCount.current - readCount;
|
||||
if (lastViewableIndex >= newMessageLineIndex) {
|
||||
resetCount();
|
||||
|
||||
@@ -9,7 +9,6 @@ import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type NewMessagesLineProps = {
|
||||
moreMessages: boolean;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
theme: Theme;
|
||||
testID?: string;
|
||||
@@ -39,34 +38,19 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
};
|
||||
});
|
||||
|
||||
function NewMessagesLine({moreMessages, style, testID, theme}: NewMessagesLineProps) {
|
||||
function NewMessagesLine({style, testID, theme}: NewMessagesLineProps) {
|
||||
const styles = getStyleFromTheme(theme);
|
||||
|
||||
let text = (
|
||||
<FormattedText
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
style={styles.text}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
|
||||
if (moreMessages) {
|
||||
text = (
|
||||
<FormattedText
|
||||
id='mobile.posts_view.moreMsg'
|
||||
defaultMessage='More New Messages Above'
|
||||
style={styles.text}
|
||||
testID={testID}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<View style={styles.line}/>
|
||||
<View style={styles.textContainer}>
|
||||
{text}
|
||||
<FormattedText
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
style={styles.text}
|
||||
testID={testID}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.line}/>
|
||||
</View>
|
||||
|
||||
@@ -4,36 +4,32 @@
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import React from 'react';
|
||||
import {of as of$, combineLatest, Observable} from 'rxjs';
|
||||
import {of as of$, combineLatest} from 'rxjs';
|
||||
import {switchMap, distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {Permissions, Preferences} from '@constants';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {Permissions, Preferences, Screens} from '@constants';
|
||||
import {observePostAuthor, queryPostsBetween} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeCanManageChannelMembers, observePermissionForPost} from '@queries/servers/role';
|
||||
import {observeIsPostPriorityEnabled, observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeIsPostPriorityEnabled} from '@queries/servers/system';
|
||||
import {observeThreadById} from '@queries/servers/thread';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {hasJumboEmojiOnly} from '@utils/emoji/helpers';
|
||||
import {areConsecutivePosts, isPostEphemeral} from '@utils/post';
|
||||
|
||||
import Post from './post';
|
||||
|
||||
import type {Database} from '@nozbe/watermelondb';
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
import type UserModel from '@typings/database/models/servers/user';
|
||||
|
||||
type PropsInput = WithDatabaseArgs & {
|
||||
appsEnabled: boolean;
|
||||
currentUser: UserModel;
|
||||
isCRTEnabled?: boolean;
|
||||
nextPost: PostModel | undefined;
|
||||
post: PostModel;
|
||||
previousPost: PostModel | undefined;
|
||||
location: string;
|
||||
}
|
||||
|
||||
function observeShouldHighlightReplyBar(database: Database, currentUser: UserModel, post: PostModel, postsInThread: PostsInThreadModel) {
|
||||
@@ -90,39 +86,35 @@ function isFirstReply(post: PostModel, previousPost?: PostModel) {
|
||||
}
|
||||
|
||||
const withSystem = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
currentUser: observeCurrentUser(database),
|
||||
}));
|
||||
|
||||
const withPost = withObservables(
|
||||
['currentUser', 'isCRTEnabled', 'post', 'previousPost', 'nextPost'],
|
||||
({currentUser, database, isCRTEnabled, post, previousPost, nextPost}: PropsInput) => {
|
||||
let isJumboEmoji = of$(false);
|
||||
({currentUser, database, isCRTEnabled, post, previousPost, nextPost, location}: PropsInput) => {
|
||||
let isLastReply = of$(true);
|
||||
let isPostAddChannelMember = of$(false);
|
||||
const isOwner = currentUser.id === post.userId;
|
||||
const author: Observable<UserModel | undefined | null> = observePostAuthor(database, post);
|
||||
const author = post.userId ? observePostAuthor(database, post) : of$(undefined);
|
||||
const canDelete = observePermissionForPost(database, post, currentUser, isOwner ? Permissions.DELETE_POST : Permissions.DELETE_OTHERS_POSTS, false);
|
||||
const isEphemeral = of$(isPostEphemeral(post));
|
||||
const isSaved = queryPreferencesByCategoryAndName(database, Preferences.CATEGORY_SAVED_POST, post.id).
|
||||
observeWithColumns(['value']).pipe(
|
||||
switchMap((pref) => of$(Boolean(pref.length))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
if (post.props?.add_channel_member && isPostEphemeral(post)) {
|
||||
isPostAddChannelMember = observeCanManageChannelMembers(database, post, currentUser);
|
||||
}
|
||||
|
||||
const highlightReplyBar = post.postsInThread.observe().pipe(
|
||||
switchMap((postsInThreads: PostsInThreadModel[]) => {
|
||||
if (postsInThreads.length) {
|
||||
return observeShouldHighlightReplyBar(database, currentUser, post, postsInThreads[0]);
|
||||
}
|
||||
return of$(false);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
let highlightReplyBar = of$(false);
|
||||
if (!isCRTEnabled && location === Screens.CHANNEL) {
|
||||
highlightReplyBar = post.postsInThread.observe().pipe(
|
||||
switchMap((postsInThreads: PostsInThreadModel[]) => {
|
||||
if (postsInThreads.length) {
|
||||
return observeShouldHighlightReplyBar(database, currentUser, post, postsInThreads[0]);
|
||||
}
|
||||
return of$(false);
|
||||
}),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
|
||||
let differentThreadSequence = true;
|
||||
if (post.rootId) {
|
||||
@@ -130,28 +122,20 @@ const withPost = withObservables(
|
||||
isLastReply = of$(!(nextPost?.rootId === post.rootId));
|
||||
}
|
||||
|
||||
if (post.message.length && !(/^\s{4}/).test(post.message)) {
|
||||
isJumboEmoji = queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap(
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
(customEmojis: CustomEmojiModel[]) => of$(hasJumboEmojiOnly(post.message, customEmojis.map((c) => c.name))),
|
||||
),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
}
|
||||
const hasReplies = observeHasReplies(post);
|
||||
const hasReplies = observeHasReplies(post);//Need to review and understand
|
||||
|
||||
const isConsecutivePost = author.pipe(
|
||||
switchMap((user) => of$(Boolean(post && previousPost && !user?.isBot && areConsecutivePosts(post, previousPost)))),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const hasFiles = post.files.observe().pipe(
|
||||
switchMap((ff) => of$(Boolean(ff.length))),
|
||||
const hasFiles = post.files.observeCount().pipe(
|
||||
switchMap((c) => of$(c > 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
const hasReactions = post.reactions.observe().pipe(
|
||||
switchMap((rr) => of$(Boolean(rr.length))),
|
||||
const hasReactions = post.reactions.observeCount().pipe(
|
||||
switchMap((c) => of$(c > 0)),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
@@ -164,8 +148,6 @@ const withPost = withObservables(
|
||||
isConsecutivePost,
|
||||
isEphemeral,
|
||||
isFirstReply: of$(isFirstReply(post, previousPost)),
|
||||
isSaved,
|
||||
isJumboEmoji,
|
||||
isLastReply,
|
||||
isPostAddChannelMember,
|
||||
isPostPriorityEnabled: observeIsPostPriorityEnabled(database),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
|
||||
import {hasJumboEmojiOnly} from '@utils/emoji/helpers';
|
||||
import {fromAutoResponder, isFromWebhook, isPostFailed, isPostPendingOrFailed, isSystemMessage} from '@utils/post';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
@@ -39,6 +40,7 @@ type PostProps = {
|
||||
appsEnabled: boolean;
|
||||
canDelete: boolean;
|
||||
currentUser: UserModel;
|
||||
customEmojiNames: string[];
|
||||
differentThreadSequence: boolean;
|
||||
hasFiles: boolean;
|
||||
hasReplies: boolean;
|
||||
@@ -50,7 +52,6 @@ type PostProps = {
|
||||
isEphemeral: boolean;
|
||||
isFirstReply?: boolean;
|
||||
isSaved?: boolean;
|
||||
isJumboEmoji: boolean;
|
||||
isLastReply?: boolean;
|
||||
isPostAddChannelMember: boolean;
|
||||
isPostPriorityEnabled: boolean;
|
||||
@@ -107,8 +108,8 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
});
|
||||
|
||||
const Post = ({
|
||||
appsEnabled, canDelete, currentUser, differentThreadSequence, hasFiles, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
|
||||
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isJumboEmoji, isLastReply, isPostAddChannelMember, isPostPriorityEnabled,
|
||||
appsEnabled, canDelete, currentUser, customEmojiNames, differentThreadSequence, hasFiles, hasReplies, highlight, highlightPinnedOrSaved = true, highlightReplyBar,
|
||||
isCRTEnabled, isConsecutivePost, isEphemeral, isFirstReply, isSaved, isLastReply, isPostAddChannelMember, isPostPriorityEnabled,
|
||||
location, post, rootId, hasReactions, searchPatterns, shouldRenderReplyButton, skipSavedHeader, skipPinnedHeader, showAddReaction = true, style,
|
||||
testID, thread, previousPost,
|
||||
}: PostProps) => {
|
||||
@@ -136,6 +137,12 @@ const Post = ({
|
||||
|
||||
return false;
|
||||
}, [isConsecutivePost, post, previousPost, isFirstReply]);
|
||||
const isJumboEmoji = useMemo(() => {
|
||||
if (post.message.length && !(/^\s{4}/).test(post.message)) {
|
||||
return hasJumboEmojiOnly(post.message, customEmojiNames);
|
||||
}
|
||||
return false;
|
||||
}, [customEmojiNames, post.message]);
|
||||
|
||||
const handlePostPress = () => {
|
||||
if ([Screens.SAVED_MESSAGES, Screens.MENTIONS, Screens.SEARCH, Screens.PINNED_MESSAGES].includes(location)) {
|
||||
|
||||
@@ -15,21 +15,23 @@ import ThreadOverview from '@components/post_list/thread_overview';
|
||||
import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {getDateForDateLine, isCombinedUserActivityPost, isDateLine, isStartOfNewMessages, isThreadOverview, preparePostList, START_OF_NEW_MESSAGES} from '@utils/post_list';
|
||||
import {getDateForDateLine, preparePostList} from '@utils/post_list';
|
||||
|
||||
import {INITIAL_BATCH_TO_RENDER, SCROLL_POSITION_CONFIG, VIEWABILITY_CONFIG} from './config';
|
||||
import MoreMessages from './more_messages';
|
||||
import PostListRefreshControl from './refresh_control';
|
||||
|
||||
import type {ViewableItemsChanged, ViewableItemsChangedListenerEvent} from '@typings/components/post_list';
|
||||
import type {PostListItem, PostListOtherItem, ViewableItemsChanged, ViewableItemsChangedListenerEvent} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
channelId: string;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
currentTimezone: string | null;
|
||||
currentUserId: string;
|
||||
currentUsername: string;
|
||||
customEmojiNames: string[];
|
||||
disablePullToRefresh?: boolean;
|
||||
highlightedId?: PostModel['id'];
|
||||
highlightPinnedOrSaved?: boolean;
|
||||
@@ -50,6 +52,7 @@ type Props = {
|
||||
testID: string;
|
||||
currentCallBarVisible?: boolean;
|
||||
joinCallBannerVisible?: boolean;
|
||||
savedPostIds: Set<string>;
|
||||
}
|
||||
|
||||
type onScrollEndIndexListenerEvent = (endIndex: number) => void;
|
||||
@@ -61,7 +64,7 @@ type ScrollIndexFailed = {
|
||||
};
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
const keyExtractor = (item: string | PostModel) => (typeof item === 'string' ? item : item.id);
|
||||
const keyExtractor = (item: PostListItem | PostListOtherItem) => (item.type === 'post' ? item.value.id : item.value);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {
|
||||
@@ -81,11 +84,13 @@ const styles = StyleSheet.create({
|
||||
});
|
||||
|
||||
const PostList = ({
|
||||
appsEnabled,
|
||||
channelId,
|
||||
contentContainerStyle,
|
||||
currentTimezone,
|
||||
currentUserId,
|
||||
currentUsername,
|
||||
customEmojiNames,
|
||||
disablePullToRefresh,
|
||||
footer,
|
||||
header,
|
||||
@@ -106,6 +111,7 @@ const PostList = ({
|
||||
testID,
|
||||
currentCallBarVisible,
|
||||
joinCallBannerVisible,
|
||||
savedPostIds,
|
||||
}: Props) => {
|
||||
const listRef = useRef<FlatList<string | PostModel>>(null);
|
||||
const onScrollEndIndexListener = useRef<onScrollEndIndexListenerEvent>();
|
||||
@@ -116,11 +122,11 @@ const PostList = ({
|
||||
const theme = useTheme();
|
||||
const serverUrl = useServerUrl();
|
||||
const orderedPosts = useMemo(() => {
|
||||
return preparePostList(posts, lastViewedAt, showNewMessageLine, currentUserId, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, currentTimezone, location === Screens.THREAD);
|
||||
}, [posts, lastViewedAt, showNewMessageLine, currentTimezone, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, location]);
|
||||
return preparePostList(posts, lastViewedAt, showNewMessageLine, currentUserId, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, currentTimezone, location === Screens.THREAD, savedPostIds);
|
||||
}, [posts, lastViewedAt, showNewMessageLine, currentTimezone, currentUsername, shouldShowJoinLeaveMessages, isTimezoneEnabled, location, savedPostIds]);
|
||||
|
||||
const initialIndex = useMemo(() => {
|
||||
return orderedPosts.indexOf(START_OF_NEW_MESSAGES);
|
||||
return orderedPosts.findIndex((i) => i.type === 'start-of-new-messages');
|
||||
}, [orderedPosts]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -220,48 +226,40 @@ const PostList = ({
|
||||
return removeListener;
|
||||
}, []);
|
||||
|
||||
const renderItem = useCallback(({item, index}: ListRenderItemInfo<string | PostModel>) => {
|
||||
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;
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
case 'start-of-new-messages':
|
||||
return (
|
||||
<NewMessagesLine
|
||||
key={item.value}
|
||||
theme={theme}
|
||||
moreMessages={moreNewMessages && checkForPostId}
|
||||
testID={`${testID}.new_messages_line`}
|
||||
style={styles.scale}
|
||||
/>
|
||||
);
|
||||
} else if (isDateLine(item)) {
|
||||
case 'date':
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
key={item.value}
|
||||
date={getDateForDateLine(item.value)}
|
||||
style={styles.scale}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
} else if (isThreadOverview(item)) {
|
||||
case 'thread-overview':
|
||||
return (
|
||||
<ThreadOverview
|
||||
key={item.value}
|
||||
rootId={rootId!}
|
||||
testID={`${testID}.thread_overview`}
|
||||
style={styles.scale}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (isCombinedUserActivityPost(item)) {
|
||||
case 'user-activity': {
|
||||
const postProps = {
|
||||
currentUsername,
|
||||
postId: item,
|
||||
key: item.value,
|
||||
postId: item.value,
|
||||
location,
|
||||
style: Platform.OS === 'ios' ? styles.scale : styles.container,
|
||||
testID: `${testID}.combined_user_activity`,
|
||||
@@ -271,71 +269,32 @@ const PostList = ({
|
||||
|
||||
return (<CombinedUserActivity {...postProps}/>);
|
||||
}
|
||||
default: {
|
||||
const post = item.value;
|
||||
const skipSaveddHeader = (location === Screens.THREAD && post.id === rootId);
|
||||
const postProps = {
|
||||
appsEnabled,
|
||||
customEmojiNames,
|
||||
isCRTEnabled,
|
||||
highlight: highlightedId === post.id,
|
||||
highlightPinnedOrSaved,
|
||||
isSaved: post.isSaved,
|
||||
key: post.id,
|
||||
location,
|
||||
nextPost: post.nextPost,
|
||||
post,
|
||||
previousPost: post.previousPost,
|
||||
rootId,
|
||||
shouldRenderReplyButton,
|
||||
skipSaveddHeader,
|
||||
style: styles.scale,
|
||||
testID: `${testID}.post`,
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let previousPost: PostModel|undefined;
|
||||
let nextPost: PostModel|undefined;
|
||||
|
||||
const lastPosts = orderedPosts.slice(index + 1);
|
||||
const immediateLastPost = lastPosts[0];
|
||||
|
||||
// Post after `Thread Overview` should show user avatar irrespective of being the consecutive post
|
||||
// So we skip sending previous post to avoid the check for consecutive post
|
||||
const skipFindingPreviousPost = (
|
||||
location === Screens.THREAD &&
|
||||
typeof immediateLastPost === 'string' &&
|
||||
isThreadOverview(immediateLastPost)
|
||||
);
|
||||
|
||||
if (!skipFindingPreviousPost) {
|
||||
const prev = lastPosts.find((v) => typeof v !== 'string');
|
||||
if (prev) {
|
||||
previousPost = prev as PostModel;
|
||||
return (<Post {...postProps}/>);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip rendering Flag for the root post in the thread as it is visible in the `Thread Overview`
|
||||
const post = item;
|
||||
const skipSaveddHeader = (
|
||||
location === Screens.THREAD &&
|
||||
post.id === rootId
|
||||
);
|
||||
|
||||
const postProps = {
|
||||
highlight: highlightedId === post.id,
|
||||
highlightPinnedOrSaved,
|
||||
location,
|
||||
nextPost,
|
||||
previousPost,
|
||||
shouldRenderReplyButton,
|
||||
skipSaveddHeader,
|
||||
};
|
||||
|
||||
return (
|
||||
<Post
|
||||
isCRTEnabled={isCRTEnabled}
|
||||
key={post.id}
|
||||
post={post}
|
||||
rootId={rootId}
|
||||
style={styles.scale}
|
||||
testID={`${testID}.post`}
|
||||
{...postProps}
|
||||
/>
|
||||
);
|
||||
}, [currentTimezone, highlightPinnedOrSaved, isCRTEnabled, isTimezoneEnabled, orderedPosts, shouldRenderReplyButton, theme]);
|
||||
}, [appsEnabled, currentTimezone, customEmojiNames, highlightPinnedOrSaved, isCRTEnabled, isTimezoneEnabled, shouldRenderReplyButton, theme]);
|
||||
|
||||
const scrollToIndex = useCallback((index: number, animated = true, applyOffset = true) => {
|
||||
listRef.current?.scrollToIndex({
|
||||
@@ -351,7 +310,7 @@ const PostList = ({
|
||||
if (highlightedId && orderedPosts && !scrolledToHighlighted.current) {
|
||||
scrolledToHighlighted.current = true;
|
||||
// eslint-disable-next-line max-nested-callbacks
|
||||
const index = orderedPosts.findIndex((p) => typeof p !== 'string' && p.id === highlightedId);
|
||||
const index = orderedPosts.findIndex((p) => p.type === 'post' && p.value.id === highlightedId);
|
||||
if (index >= 0 && listRef.current) {
|
||||
listRef.current?.scrollToIndex({
|
||||
animated: true,
|
||||
@@ -387,7 +346,7 @@ const PostList = ({
|
||||
maxToRenderPerBatch={10}
|
||||
nativeID={nativeID}
|
||||
onEndReached={onEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
onEndReachedThreshold={0.9}
|
||||
onScroll={onScroll}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
onViewableItemsChanged={onViewableItemsChanged}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import compose from 'lodash/fp/compose';
|
||||
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
|
||||
@@ -17,7 +16,4 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
};
|
||||
});
|
||||
|
||||
export default compose(
|
||||
withDatabase,
|
||||
enhance,
|
||||
)(PostWithChannelInfo);
|
||||
export default withDatabase(enhance(PostWithChannelInfo));
|
||||
|
||||
@@ -11,6 +11,8 @@ import ChannelInfo from './channel_info';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
customEmojiNames: string[];
|
||||
isCRTEnabled: boolean;
|
||||
post: PostModel;
|
||||
location: string;
|
||||
@@ -28,7 +30,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
function PostWithChannelInfo({isCRTEnabled, post, location, testID}: Props) {
|
||||
function PostWithChannelInfo({appsEnabled, customEmojiNames, isCRTEnabled, post, location, testID}: Props) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ChannelInfo
|
||||
@@ -37,6 +39,8 @@ function PostWithChannelInfo({isCRTEnabled, post, location, testID}: Props) {
|
||||
/>
|
||||
<View style={styles.content}>
|
||||
<Post
|
||||
appsEnabled={appsEnabled}
|
||||
customEmojiNames={customEmojiNames}
|
||||
isCRTEnabled={isCRTEnabled}
|
||||
post={post}
|
||||
location={location}
|
||||
|
||||
@@ -14,8 +14,9 @@ import {observeUser} from './user';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type PostInChannelModel from '@typings/database/models/servers/posts_in_channel';
|
||||
import type PostsInThreadModel from '@typings/database/models/servers/posts_in_thread';
|
||||
import type PreferenceModel from '@typings/database/models/servers/preference';
|
||||
|
||||
const {SERVER: {POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD}} = MM_TABLES;
|
||||
const {SERVER: {POST, POSTS_IN_CHANNEL, POSTS_IN_THREAD, PREFERENCE}} = MM_TABLES;
|
||||
|
||||
export const prepareDeletePost = async (post: PostModel): Promise<Model[]> => {
|
||||
const preparedModels: Model[] = [post.prepareDestroyPermanently()];
|
||||
@@ -74,7 +75,7 @@ export const observePost = (database: Database, postId: string) => {
|
||||
};
|
||||
|
||||
export const observePostAuthor = (database: Database, post: PostModel) => {
|
||||
return post.userId ? observeUser(database, post.userId) : of$(null);
|
||||
return observeUser(database, post.userId);
|
||||
};
|
||||
|
||||
export const observePostSaved = (database: Database, postId: string) => {
|
||||
@@ -216,3 +217,15 @@ export const queryPinnedPostsInChannel = (database: Database, channelId: string)
|
||||
export const observePinnedPostsInChannel = (database: Database, channelId: string) => {
|
||||
return queryPinnedPostsInChannel(database, channelId).observe();
|
||||
};
|
||||
|
||||
export const observeSavedPostsByIds = (database: Database, postIds: string[]) => {
|
||||
return database.get<PreferenceModel>(PREFERENCE).
|
||||
query(
|
||||
Q.and(
|
||||
Q.where('category', Preferences.CATEGORY_SAVED_POST),
|
||||
Q.where('name', Q.oneOf(postIds)),
|
||||
),
|
||||
).observeWithColumns(['name']).pipe(
|
||||
switchMap((prefs) => of$(new Set(prefs.map((p) => p.name)))),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -149,6 +149,7 @@ const ThreadsList = ({
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<ThreadModel>) => (
|
||||
<Thread
|
||||
location={Screens.GLOBAL_THREADS}
|
||||
key={item.id}
|
||||
testID={testID}
|
||||
teammateNameDisplay={teammateNameDisplay}
|
||||
thread={item}
|
||||
|
||||
@@ -8,9 +8,11 @@ import compose from 'lodash/fp/compose';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {queryPostsById} from '@queries/servers/post';
|
||||
import {observeConfigBooleanValue, observeRecentMentions} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import RecentMentionsScreen from './recent_mentions';
|
||||
@@ -21,6 +23,7 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
const currentUser = observeCurrentUser(database);
|
||||
|
||||
return {
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
mentions: observeRecentMentions(database).pipe(
|
||||
switchMap((recentMentions) => {
|
||||
if (!recentMentions.length) {
|
||||
@@ -31,6 +34,9 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
),
|
||||
currentUser,
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone || null))))),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,17 +17,19 @@ import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import {getDateForDateLine, isDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
import EmptyState from './components/empty';
|
||||
|
||||
import type {ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type {PostListItem, PostListOtherItem, ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);
|
||||
const EDGES: Edge[] = ['bottom', 'left', 'right'];
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
customEmojiNames: string[];
|
||||
currentTimezone: string | null;
|
||||
isTimezoneEnabled: boolean;
|
||||
mentions: PostModel[];
|
||||
@@ -47,7 +49,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Props) => {
|
||||
const RecentMentionsScreen = ({appsEnabled, customEmojiNames, mentions, currentTimezone, isTimezoneEnabled}: Props) => {
|
||||
const theme = useTheme();
|
||||
const route = useRoute();
|
||||
const isFocused = useIsFocused();
|
||||
@@ -134,27 +136,31 @@ const RecentMentionsScreen = ({mentions, currentTimezone, isTimezoneEnabled}: Pr
|
||||
</View>
|
||||
), [loading, theme, paddingTop]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<string | PostModel>) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
case 'date':
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
key={item.value}
|
||||
date={getDateForDateLine(item.value)}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case 'post':
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
appsEnabled={appsEnabled}
|
||||
customEmojiNames={customEmojiNames}
|
||||
key={item.value.id}
|
||||
location={Screens.MENTIONS}
|
||||
post={item.value}
|
||||
testID='recent_mentions.post_list'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
location={Screens.MENTIONS}
|
||||
post={item}
|
||||
testID='recent_mentions.post_list'
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
}, [appsEnabled, customEmojiNames]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -9,10 +9,12 @@ import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {Preferences} from '@constants';
|
||||
import {PreferenceModel} from '@database/models/server';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {queryPostsById} from '@queries/servers/post';
|
||||
import {queryPreferencesByCategoryAndName} from '@queries/servers/preference';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import SavedMessagesScreen from './saved_messages';
|
||||
@@ -39,6 +41,9 @@ const enhance = withObservables([], ({database}: WithDatabaseArgs) => {
|
||||
}),
|
||||
),
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone || null))))),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -18,15 +18,17 @@ import {Events, Screens} from '@constants';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useCollapsibleHeader} from '@hooks/header';
|
||||
import {isDateLine, getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
import EmptyState from './components/empty';
|
||||
|
||||
import type {ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type {PostListItem, PostListOtherItem, ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
currentTimezone: string | null;
|
||||
customEmojiNames: string[];
|
||||
isTimezoneEnabled: boolean;
|
||||
posts: PostModel[];
|
||||
}
|
||||
@@ -48,7 +50,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
function SavedMessages({appsEnabled, posts, currentTimezone, customEmojiNames, isTimezoneEnabled}: Props) {
|
||||
const intl = useIntl();
|
||||
const [loading, setLoading] = useState(!posts.length);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
@@ -135,27 +137,31 @@ function SavedMessages({posts, currentTimezone, isTimezoneEnabled}: Props) {
|
||||
</View>
|
||||
), [loading, theme.buttonBg]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<string | PostModel>) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
case 'date':
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
key={item.value}
|
||||
date={getDateForDateLine(item.value)}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case 'post':
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
appsEnabled={appsEnabled}
|
||||
customEmojiNames={customEmojiNames}
|
||||
key={item.value.id}
|
||||
location={Screens.SAVED_MESSAGES}
|
||||
post={item.value}
|
||||
testID='saved_messages.post_list'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
location={Screens.SAVED_MESSAGES}
|
||||
post={item}
|
||||
testID='saved_messages.post_list'
|
||||
/>
|
||||
);
|
||||
}, [currentTimezone, isTimezoneEnabled, theme]);
|
||||
}, [appsEnabled, currentTimezone, customEmojiNames, isTimezoneEnabled, theme]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -94,6 +94,7 @@ const FileResults = ({
|
||||
channelName={channelNames[item.channel_id!]}
|
||||
fileInfo={item}
|
||||
index={fileInfosIndexes[item.id!] || 0}
|
||||
key={`${item.id}-${item.name}`}
|
||||
numOptions={numOptions}
|
||||
onOptionsPress={onOptionsPress}
|
||||
onPress={onPreviewPress}
|
||||
|
||||
@@ -8,8 +8,10 @@ import {combineLatest, of as of$} from 'rxjs';
|
||||
import {map, switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryChannelsById} from '@queries/servers/channel';
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observeLicense, observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import Results from './results';
|
||||
@@ -35,7 +37,11 @@ const enhance = withObservables(['fileChannelIds'], ({database, fileChannelIds}:
|
||||
);
|
||||
|
||||
return {
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone))))),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
fileChannels,
|
||||
canDownloadFiles,
|
||||
|
||||
@@ -8,12 +8,15 @@ import NoResultsWithTerm from '@components/no_results_with_term';
|
||||
import DateSeparator from '@components/post_list/date_separator';
|
||||
import PostWithChannelInfo from '@components/post_with_channel_info';
|
||||
import {Screens} from '@constants';
|
||||
import {getDateForDateLine, isDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
import {TabTypes} from '@utils/search';
|
||||
|
||||
import type {PostListItem, PostListOtherItem} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
customEmojiNames: string[];
|
||||
currentTimezone: string;
|
||||
isTimezoneEnabled: boolean;
|
||||
posts: PostModel[];
|
||||
@@ -22,7 +25,9 @@ type Props = {
|
||||
}
|
||||
|
||||
const PostResults = ({
|
||||
appsEnabled,
|
||||
currentTimezone,
|
||||
customEmojiNames,
|
||||
isTimezoneEnabled,
|
||||
posts,
|
||||
paddingTop,
|
||||
@@ -31,30 +36,31 @@ const PostResults = ({
|
||||
const orderedPosts = useMemo(() => selectOrderedPosts(posts, 0, false, '', '', false, isTimezoneEnabled, currentTimezone, false).reverse(), [posts]);
|
||||
const containerStyle = useMemo(() => ({top: posts.length ? 4 : 8}), [posts]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<string|PostModel>) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
case 'date':
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
key={item.value}
|
||||
date={getDateForDateLine(item.value)}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case 'post':
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
appsEnabled={appsEnabled}
|
||||
customEmojiNames={customEmojiNames}
|
||||
key={item.value.id}
|
||||
location={Screens.SEARCH}
|
||||
post={item.value}
|
||||
testID='search_results.post_list'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ('message' in item) {
|
||||
return (
|
||||
<PostWithChannelInfo
|
||||
location={Screens.SEARCH}
|
||||
post={item}
|
||||
testID='search_results.post_list'
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
}, [appsEnabled, customEmojiNames]);
|
||||
|
||||
const noResults = useMemo(() => (
|
||||
<NoResultsWithTerm
|
||||
|
||||
@@ -37,8 +37,10 @@ const getStyles = (width: number) => {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
canDownloadFiles: boolean;
|
||||
currentTimezone: string;
|
||||
customEmojiNames: string[];
|
||||
fileChannels: ChannelModel[];
|
||||
fileInfos: FileInfo[];
|
||||
isTimezoneEnabled: boolean;
|
||||
@@ -51,8 +53,10 @@ type Props = {
|
||||
}
|
||||
|
||||
const Results = ({
|
||||
appsEnabled,
|
||||
canDownloadFiles,
|
||||
currentTimezone,
|
||||
customEmojiNames,
|
||||
fileChannels,
|
||||
fileInfos,
|
||||
isTimezoneEnabled,
|
||||
@@ -96,7 +100,9 @@ const Results = ({
|
||||
<Animated.View style={[styles.container, transform]}>
|
||||
<View style={styles.result} >
|
||||
<PostResults
|
||||
appsEnabled={appsEnabled}
|
||||
currentTimezone={currentTimezone}
|
||||
customEmojiNames={customEmojiNames}
|
||||
isTimezoneEnabled={isTimezoneEnabled}
|
||||
posts={posts}
|
||||
paddingTop={paddingTop}
|
||||
|
||||
@@ -6,10 +6,12 @@ import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {queryAllCustomEmojis} from '@queries/servers/custom_emoji';
|
||||
import {observePinnedPostsInChannel} from '@queries/servers/post';
|
||||
import {observeConfigBooleanValue} from '@queries/servers/system';
|
||||
import {observeIsCRTEnabled} from '@queries/servers/thread';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {mapCustomEmojiNames} from '@utils/emoji/helpers';
|
||||
import {getTimezone} from '@utils/user';
|
||||
|
||||
import PinnedMessages from './pinned_messages';
|
||||
@@ -25,7 +27,11 @@ const enhance = withObservables(['channelId'], ({channelId, database}: Props) =>
|
||||
const posts = observePinnedPostsInChannel(database, channelId);
|
||||
|
||||
return {
|
||||
appsEnabled: observeConfigBooleanValue(database, 'FeatureFlagAppsEnabled'),
|
||||
currentTimezone: currentUser.pipe((switchMap((user) => of$(getTimezone(user?.timezone || null))))),
|
||||
customEmojiNames: queryAllCustomEmojis(database).observe().pipe(
|
||||
switchMap((customEmojis) => of$(mapCustomEmojiNames(customEmojis))),
|
||||
),
|
||||
isCRTEnabled: observeIsCRTEnabled(database),
|
||||
isTimezoneEnabled: observeConfigBooleanValue(database, 'ExperimentalTimezone'),
|
||||
posts,
|
||||
|
||||
@@ -14,17 +14,19 @@ import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {getDateForDateLine, isDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
import EmptyState from './empty';
|
||||
|
||||
import type {ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type {PostListItem, PostListOtherItem, ViewableItemsChanged} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
type Props = {
|
||||
appsEnabled: boolean;
|
||||
channelId: string;
|
||||
componentId: string;
|
||||
currentTimezone: string | null;
|
||||
customEmojiNames: string[];
|
||||
isCRTEnabled: boolean;
|
||||
isTimezoneEnabled: boolean;
|
||||
posts: PostModel[];
|
||||
@@ -47,9 +49,11 @@ const styles = StyleSheet.create({
|
||||
});
|
||||
|
||||
function SavedMessages({
|
||||
appsEnabled,
|
||||
channelId,
|
||||
componentId,
|
||||
currentTimezone,
|
||||
customEmojiNames,
|
||||
isCRTEnabled,
|
||||
isTimezoneEnabled,
|
||||
posts,
|
||||
@@ -109,35 +113,39 @@ function SavedMessages({
|
||||
</View>
|
||||
), [loading, theme.buttonBg]);
|
||||
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<string | PostModel>) => {
|
||||
if (typeof item === 'string') {
|
||||
if (isDateLine(item)) {
|
||||
const renderItem = useCallback(({item}: ListRenderItemInfo<PostListItem | PostListOtherItem>) => {
|
||||
switch (item.type) {
|
||||
case 'date':
|
||||
return (
|
||||
<DateSeparator
|
||||
date={getDateForDateLine(item)}
|
||||
key={item.value}
|
||||
date={getDateForDateLine(item.value)}
|
||||
timezone={isTimezoneEnabled ? currentTimezone : null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
case 'post':
|
||||
return (
|
||||
<Post
|
||||
appsEnabled={appsEnabled}
|
||||
customEmojiNames={customEmojiNames}
|
||||
highlightPinnedOrSaved={false}
|
||||
isCRTEnabled={isCRTEnabled}
|
||||
location={Screens.PINNED_MESSAGES}
|
||||
key={item.value.id}
|
||||
nextPost={undefined}
|
||||
post={item.value}
|
||||
previousPost={undefined}
|
||||
showAddReaction={false}
|
||||
shouldRenderReplyButton={false}
|
||||
skipSavedHeader={true}
|
||||
skipPinnedHeader={true}
|
||||
testID='pinned_messages.post_list.post'
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Post
|
||||
highlightPinnedOrSaved={false}
|
||||
isCRTEnabled={isCRTEnabled}
|
||||
location={Screens.PINNED_MESSAGES}
|
||||
nextPost={undefined}
|
||||
post={item}
|
||||
previousPost={undefined}
|
||||
showAddReaction={false}
|
||||
shouldRenderReplyButton={false}
|
||||
skipSavedHeader={true}
|
||||
skipPinnedHeader={true}
|
||||
testID='pinned_messages.post_list.post'
|
||||
/>
|
||||
);
|
||||
}, [currentTimezone, isTimezoneEnabled, theme]);
|
||||
}, [appsEnabled, currentTimezone, customEmojiNames, isTimezoneEnabled, theme]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
|
||||
@@ -212,6 +212,10 @@ export function getEmojiByName(emojiName: string, customEmojis: CustomEmojiModel
|
||||
return customEmojis.find((e) => e.name === emojiName);
|
||||
}
|
||||
|
||||
export function mapCustomEmojiNames(customEmois: CustomEmojiModel[]) {
|
||||
return customEmois.map((c) => c.name);
|
||||
}
|
||||
|
||||
// Since there is no shared logic between the web and mobile app
|
||||
// this is copied from the webapp as custom sorting logic for emojis
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {Post} from '@constants';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {isFromWebhook} from '@utils/post';
|
||||
|
||||
import type {PostList, PostWithPrevAndNext} from '@typings/components/post_list';
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
|
||||
const joinLeavePostTypes = [
|
||||
@@ -49,44 +50,37 @@ export const START_OF_NEW_MESSAGES = 'start-of-new-messages';
|
||||
export const THREAD_OVERVIEW = 'thread-overview';
|
||||
export const MAX_COMBINED_SYSTEM_POSTS = 100;
|
||||
|
||||
function combineUserActivityPosts(orderedPosts: Array<PostModel | string>) {
|
||||
function combineUserActivityPosts(orderedPosts: PostList) {
|
||||
let lastPostIsUserActivity = false;
|
||||
let combinedCount = 0;
|
||||
const out: Array<PostModel | string> = [];
|
||||
const out: PostList = [];
|
||||
let changed = false;
|
||||
|
||||
for (let i = 0; i < orderedPosts.length; i++) {
|
||||
const post = orderedPosts[i];
|
||||
const item = orderedPosts[i];
|
||||
if (item.type === 'start-of-new-messages' || item.type === 'date' || item.type === 'thread-overview') {
|
||||
// Not a post, so it won't be combined
|
||||
out.push(item);
|
||||
|
||||
if (typeof post === 'string') {
|
||||
if (post === START_OF_NEW_MESSAGES || post.startsWith(DATE_LINE)) {
|
||||
// Not a post, so it won't be combined
|
||||
out.push(post);
|
||||
lastPostIsUserActivity = false;
|
||||
combinedCount = 0;
|
||||
|
||||
lastPostIsUserActivity = false;
|
||||
combinedCount = 0;
|
||||
|
||||
continue;
|
||||
}
|
||||
} else if (post.deleteAt) {
|
||||
out.push(post);
|
||||
continue;
|
||||
} else if (item.type === 'post' && item.value.deleteAt) {
|
||||
out.push(item);
|
||||
|
||||
lastPostIsUserActivity = false;
|
||||
combinedCount = 0;
|
||||
} else {
|
||||
const postIsUserActivity = Post.USER_ACTIVITY_POST_TYPES.includes(post.type);
|
||||
const postIsUserActivity = item.type === 'post' && Post.USER_ACTIVITY_POST_TYPES.includes(item.value.type);
|
||||
if (postIsUserActivity && lastPostIsUserActivity && combinedCount < MAX_COMBINED_SYSTEM_POSTS) {
|
||||
// Add the ID to the previous combined post
|
||||
out[out.length - 1] += '_' + post.id;
|
||||
combinedCount += 1;
|
||||
changed = true;
|
||||
out[out.length - 1].value += '_' + item.value.id;
|
||||
} else if (postIsUserActivity) {
|
||||
// Start a new combined post, even if the "combined" post is only a single post
|
||||
out.push(COMBINED_USER_ACTIVITY + post.id);
|
||||
out.push({type: 'user-activity', value: `${COMBINED_USER_ACTIVITY}${item.value.id}`});
|
||||
combinedCount = 1;
|
||||
changed = true;
|
||||
} else {
|
||||
out.push(post);
|
||||
out.push(item);
|
||||
combinedCount = 0;
|
||||
}
|
||||
|
||||
@@ -175,21 +169,38 @@ function isJoinLeavePostForUsername(post: PostModel, currentUsername: string): b
|
||||
post.props.removedUsername === currentUsername;
|
||||
}
|
||||
|
||||
// are we going to do something with selectedPostId as in v1?
|
||||
export function selectOrderedPostsWithPrevAndNext(
|
||||
posts: PostModel[], lastViewedAt: number, indicateNewMessages: boolean, currentUserId: string, currentUsername: string, showJoinLeave: boolean,
|
||||
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false, savedPostIds = new Set<string>(),
|
||||
): PostList {
|
||||
return selectOrderedPosts(
|
||||
posts, lastViewedAt, indicateNewMessages,
|
||||
currentUserId, currentUsername, showJoinLeave,
|
||||
timezoneEnabled, currentTimezone, isThreadScreen, savedPostIds, true,
|
||||
);
|
||||
}
|
||||
|
||||
export function selectOrderedPosts(
|
||||
posts: PostModel[], lastViewedAt: number, indicateNewMessages: boolean, currentUserId: string, currentUsername: string, showJoinLeave: boolean,
|
||||
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false) {
|
||||
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false, savedPostIds = new Set<string>(), includePrevNext = false) {
|
||||
if (posts.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const out: Array<PostModel|string> = [];
|
||||
const out: PostList = [];
|
||||
let lastDate;
|
||||
let addedNewMessagesIndicator = false;
|
||||
|
||||
// Iterating through the posts from oldest to newest
|
||||
for (let i = posts.length - 1; i >= 0; i--) {
|
||||
const post = posts[i];
|
||||
const post: PostWithPrevAndNext = posts[i];
|
||||
post.isSaved = savedPostIds.has(post.id);
|
||||
if (includePrevNext) {
|
||||
post.nextPost = posts[i - 1];
|
||||
if (!isThreadScreen || out[out.length - 1]?.type !== 'thread-overview') {
|
||||
post.previousPost = posts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!post ||
|
||||
@@ -217,7 +228,7 @@ export function selectOrderedPosts(
|
||||
}
|
||||
|
||||
if (!lastDate || lastDate.toDateString() !== postDate.toDateString()) {
|
||||
out.push(DATE_LINE + postDate.getTime());
|
||||
out.push({type: 'date', value: DATE_LINE + postDate.getTime()});
|
||||
|
||||
lastDate = postDate;
|
||||
}
|
||||
@@ -229,14 +240,14 @@ export function selectOrderedPosts(
|
||||
!addedNewMessagesIndicator &&
|
||||
indicateNewMessages
|
||||
) {
|
||||
out.push(START_OF_NEW_MESSAGES);
|
||||
out.push({type: 'start-of-new-messages', value: START_OF_NEW_MESSAGES});
|
||||
addedNewMessagesIndicator = true;
|
||||
}
|
||||
|
||||
out.push(post);
|
||||
out.push({type: 'post', value: post});
|
||||
|
||||
if (isThreadScreen && i === posts.length - 1) {
|
||||
out.push(THREAD_OVERVIEW);
|
||||
out.push({type: 'thread-overview', value: THREAD_OVERVIEW});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,26 +361,10 @@ export function getPostIdsForCombinedUserActivityPost(item: string) {
|
||||
return item.substring(COMBINED_USER_ACTIVITY.length).split('_');
|
||||
}
|
||||
|
||||
export function isCombinedUserActivityPost(item: string) {
|
||||
return (/^user-activity-(?:[^_]+_)*[^_]+$/).test(item);
|
||||
}
|
||||
|
||||
export function isDateLine(item: string) {
|
||||
return Boolean(item?.startsWith(DATE_LINE));
|
||||
}
|
||||
|
||||
export function isStartOfNewMessages(item: string) {
|
||||
return item === START_OF_NEW_MESSAGES;
|
||||
}
|
||||
|
||||
export function isThreadOverview(item: string) {
|
||||
return item === THREAD_OVERVIEW;
|
||||
}
|
||||
|
||||
export function preparePostList(
|
||||
posts: PostModel[], lastViewedAt: number, indicateNewMessages: boolean, currentUserId: string, currentUsername: string, showJoinLeave: boolean,
|
||||
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false) {
|
||||
const orderedPosts = selectOrderedPosts(posts, lastViewedAt, indicateNewMessages, currentUserId, currentUsername, showJoinLeave, timezoneEnabled, currentTimezone, isThreadScreen);
|
||||
timezoneEnabled: boolean, currentTimezone: string | null, isThreadScreen = false, savedPostIds = new Set<string>()) {
|
||||
const orderedPosts = selectOrderedPostsWithPrevAndNext(posts, lastViewedAt, indicateNewMessages, currentUserId, currentUsername, showJoinLeave, timezoneEnabled, currentTimezone, isThreadScreen, savedPostIds);
|
||||
return combineUserActivityPosts(orderedPosts);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import type PostModel from '@typings/database/models/servers/post';
|
||||
import type {ViewToken} from 'react-native';
|
||||
|
||||
export type ViewableItemsChanged = {
|
||||
@@ -12,3 +13,17 @@ export type ViewableItemsChangedListenerEvent = (viewableItms: ViewToken[]) => v
|
||||
|
||||
export type ScrollEndIndexListener = (fn: (endIndex: number) => void) => () => void;
|
||||
export type ViewableItemsListener = (fn: (viewableItems: ViewToken[]) => void) => () => void;
|
||||
|
||||
export type PostWithPrevAndNext = PostModel & {nextPost?: PostModel; previousPost?: PostModel; isSaved?: boolean};
|
||||
|
||||
export type PostListItem = {
|
||||
type: 'post';
|
||||
value: PostWithPrevAndNext;
|
||||
}
|
||||
|
||||
export type PostListOtherItem = {
|
||||
type: 'date' | 'thread-overview' | 'start-of-new-messages' | 'user-activity';
|
||||
value: string;
|
||||
}
|
||||
|
||||
export type PostList = Array<PostListItem | PostListOtherItem>;
|
||||
|
||||
Reference in New Issue
Block a user