forked from Ivasoft/mattermost-mobile
MM-38784, MM-38409 New messages line in threads screen & Pull to refresh for global threads screen (#5690)
* New messages line in threads & Pull to refresh for global threads * Updated snapshot * Using postlist's RefreshControl component * Updated threadlist snapshot * Reverting snapshot * Updated Snapshots * Snapshots updated * Added 'thread last viewed at' to handle new messages line correctly * Lint fix * Updated snapshots * Reverted comparision check * Remove unused code * Batching actions * Do not add new message line for self messages Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
1a35250811
commit
a81b56212e
@@ -2,6 +2,16 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import {ViewTypes} from '@constants';
|
||||
|
||||
export function updateThreadLastViewedAt(threadId: string, lastViewedAt: number) {
|
||||
return {
|
||||
type: ViewTypes.THREAD_LAST_VIEWED_AT,
|
||||
data: {
|
||||
threadId,
|
||||
lastViewedAt,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const handleViewingGlobalThreadsScreen = () => (
|
||||
{
|
||||
type: ViewTypes.VIEWING_GLOBAL_THREADS_SCREEN,
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {updateThreadLastViewedAt} from '@actions/views/threads';
|
||||
import {handleThreadArrived, handleReadChanged, handleAllMarkedRead, handleFollowChanged, getThread as fetchThread} from '@mm-redux/actions/threads';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/common';
|
||||
import {getSelectedPost} from '@mm-redux/selectors/entities/posts';
|
||||
import {getCurrentTeamId} from '@mm-redux/selectors/entities/teams';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
import {ActionResult, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {ActionResult, DispatchFunc, GenericAction, GetStateFunc} from '@mm-redux/types/actions';
|
||||
import {WebSocketMessage} from '@mm-redux/types/websocket';
|
||||
|
||||
export function handleThreadUpdated(msg: WebSocketMessage) {
|
||||
@@ -27,21 +31,31 @@ export function handleThreadReadChanged(msg: WebSocketMessage) {
|
||||
const thread = getThread(state, msg.data.thread_id);
|
||||
|
||||
// Mark only following threads as read.
|
||||
if (thread?.is_following) {
|
||||
dispatch(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
if (thread) {
|
||||
const actions: GenericAction[] = [];
|
||||
const selectedPost = getSelectedPost(state);
|
||||
if (selectedPost?.id !== thread.id) {
|
||||
actions.push(updateThreadLastViewedAt(thread.id, msg.data.timestamp));
|
||||
}
|
||||
if (thread.is_following) {
|
||||
actions.push(
|
||||
handleReadChanged(
|
||||
msg.data.thread_id,
|
||||
msg.broadcast.team_id,
|
||||
msg.data.channel_id,
|
||||
{
|
||||
lastViewedAt: msg.data.timestamp,
|
||||
prevUnreadMentions: thread.unread_mentions,
|
||||
newUnreadMentions: msg.data.unread_mentions,
|
||||
prevUnreadReplies: thread.unread_replies,
|
||||
newUnreadReplies: msg.data.unread_replies,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (actions.length) {
|
||||
dispatch(batchActions(actions));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
dispatch(handleAllMarkedRead(msg.broadcast.team_id));
|
||||
|
||||
@@ -38,6 +38,7 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
const listRef = React.useRef<FlatList>(null);
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(true);
|
||||
const [isRefreshing, setIsRefreshing] = React.useState<boolean>(false);
|
||||
|
||||
const scrollToTop = () => {
|
||||
listRef.current?.scrollToOffset({offset: 0});
|
||||
@@ -79,6 +80,16 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = async () => {
|
||||
if (!isLoading) {
|
||||
if (!isRefreshing) {
|
||||
setIsRefreshing(true);
|
||||
}
|
||||
await loadThreads('', '', viewingUnreads);
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const markAllAsRead = () => {
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
@@ -112,9 +123,11 @@ function GlobalThreadsList({actions, allThreadIds, intl, teamId, theme, threadCo
|
||||
<ThreadList
|
||||
haveUnreads={haveUnreads}
|
||||
isLoading={isLoading}
|
||||
isRefreshing={isRefreshing}
|
||||
listRef={listRef}
|
||||
loadMoreThreads={loadMoreThreads}
|
||||
markAllAsRead={markAllAsRead}
|
||||
onRefresh={onRefresh}
|
||||
testID={'global_threads'}
|
||||
theme={theme}
|
||||
threadIds={ids}
|
||||
|
||||
@@ -86,62 +86,99 @@ exports[`Global Thread List Should render threads with functional tabs & mark al
|
||||
viewUnreadThreads={[MockFunction]}
|
||||
viewingUnreads={true}
|
||||
/>
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
<PostListRefreshControl
|
||||
enabled={true}
|
||||
isInverted={false}
|
||||
onRefresh={[MockFunction]}
|
||||
refreshing={false}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc1f",
|
||||
"buttonBg": "#1c58d9",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3f4350",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#d24b4e",
|
||||
"errorTextColor": "#d24b4e",
|
||||
"linkColor": "#386fe5",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionColor": "#1e325c",
|
||||
"mentionHighlightBg": "#ffd470",
|
||||
"mentionHighlightLink": "#1b1d22",
|
||||
"newMessageSeparator": "#cc8f00",
|
||||
"onlineIndicator": "#3db887",
|
||||
"sidebarBg": "#1e325c",
|
||||
"sidebarHeaderBg": "#192a4d",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarTeamBarBg": "#14213e",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#5d89ea",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#28427b",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Denim",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FlatList
|
||||
ListEmptyComponent={
|
||||
<EmptyState
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
isUnreads={true}
|
||||
/>
|
||||
}
|
||||
ListFooterComponent={null}
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"flexGrow": 1,
|
||||
}
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
numColumns={1}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
data={
|
||||
Array [
|
||||
"thread1",
|
||||
]
|
||||
}
|
||||
}
|
||||
/>
|
||||
initialNumToRender={10}
|
||||
keyExtractor={[Function]}
|
||||
numColumns={1}
|
||||
onEndReached={[Function]}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={[Function]}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={[Function]}
|
||||
scrollIndicatorInsets={
|
||||
Object {
|
||||
"right": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</PostListRefreshControl>
|
||||
</View>
|
||||
`;
|
||||
|
||||
@@ -25,9 +25,11 @@ describe('Global Thread List', () => {
|
||||
haveUnreads: true,
|
||||
intl,
|
||||
isLoading: false,
|
||||
isRefreshing: false,
|
||||
listRef: React.useRef<FlatList>(null),
|
||||
loadMoreThreads: jest.fn(),
|
||||
markAllAsRead,
|
||||
onRefresh: jest.fn(),
|
||||
testID,
|
||||
theme: Preferences.THEMES.denim,
|
||||
threadIds: ['thread1'],
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {FlatList, Platform, View} from 'react-native';
|
||||
import {FlatList, NativeSyntheticEvent, NativeScrollEvent, Platform, View} from 'react-native';
|
||||
|
||||
import EmptyState from '@components/global_threads/empty_state';
|
||||
import ThreadItem from '@components/global_threads/thread_item';
|
||||
import Loading from '@components/loading';
|
||||
import {INITIAL_BATCH_TO_RENDER} from '@components/post_list/post_list_config';
|
||||
import CustomRefreshControl from '@components/post_list/post_list_refresh_control';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
@@ -21,9 +22,11 @@ export type Props = {
|
||||
haveUnreads: boolean;
|
||||
intl: typeof intlShape;
|
||||
isLoading: boolean;
|
||||
isRefreshing: boolean;
|
||||
loadMoreThreads: () => Promise<void>;
|
||||
listRef: React.RefObject<FlatList>;
|
||||
markAllAsRead: () => void;
|
||||
onRefresh: () => void;
|
||||
testID: string;
|
||||
theme: Theme;
|
||||
threadIds: $ID<UserThread>[];
|
||||
@@ -32,9 +35,11 @@ export type Props = {
|
||||
viewingUnreads: boolean;
|
||||
};
|
||||
|
||||
function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, markAllAsRead, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
function ThreadList({haveUnreads, intl, isLoading, isRefreshing, loadMoreThreads, listRef, markAllAsRead, onRefresh, testID, theme, threadIds, viewAllThreads, viewUnreadThreads, viewingUnreads}: Props) {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const [offsetY, setOffsetY] = React.useState(0);
|
||||
|
||||
const handleEndReached = React.useCallback(() => {
|
||||
loadMoreThreads();
|
||||
}, [loadMoreThreads, viewingUnreads]);
|
||||
@@ -51,6 +56,17 @@ function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, mar
|
||||
);
|
||||
}, [theme]);
|
||||
|
||||
const onScroll = React.useCallback((event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||
if (Platform.OS === 'android') {
|
||||
const {y} = event.nativeEvent.contentOffset;
|
||||
if (y === 0) {
|
||||
setOffsetY(y);
|
||||
} else if (offsetY === 0 && y !== 0) {
|
||||
setOffsetY(y);
|
||||
}
|
||||
}
|
||||
}, [offsetY]);
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!viewingUnreads && !threadIds.length) {
|
||||
return null;
|
||||
@@ -96,21 +112,30 @@ function ThreadList({haveUnreads, intl, isLoading, loadMoreThreads, listRef, mar
|
||||
return (
|
||||
<View style={style.container}>
|
||||
{renderHeader()}
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
<CustomRefreshControl
|
||||
enabled={offsetY === 0}
|
||||
isInverted={false}
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={onRefresh}
|
||||
theme={theme}
|
||||
>
|
||||
<FlatList
|
||||
contentContainerStyle={style.messagesContainer}
|
||||
data={threadIds}
|
||||
keyExtractor={keyExtractor}
|
||||
ListEmptyComponent={renderEmptyList()}
|
||||
ListFooterComponent={renderFooter()}
|
||||
onEndReached={handleEndReached}
|
||||
onEndReachedThreshold={2}
|
||||
onScroll={onScroll}
|
||||
ref={listRef}
|
||||
renderItem={renderPost}
|
||||
initialNumToRender={INITIAL_BATCH_TO_RENDER}
|
||||
maxToRenderPerBatch={Platform.select({android: 5})}
|
||||
removeClippedSubviews={true}
|
||||
scrollIndicatorInsets={style.listScrollIndicator}
|
||||
/>
|
||||
</CustomRefreshControl>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {Theme} from '@mm-redux/types/theme';
|
||||
type Props = {
|
||||
children: ReactElement;
|
||||
enabled: boolean;
|
||||
isInverted?: boolean;
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
theme: Theme;
|
||||
@@ -17,11 +18,13 @@ type Props = {
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
containerInverse: {
|
||||
scaleY: -1,
|
||||
},
|
||||
});
|
||||
|
||||
const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme}: Props) => {
|
||||
const PostListRefreshControl = ({children, enabled, isInverted = true, onRefresh, refreshing, theme}: Props) => {
|
||||
const props = {
|
||||
colors: [theme.onlineIndicator, theme.awayIndicator, theme.dndIndicator],
|
||||
onRefresh,
|
||||
@@ -34,7 +37,7 @@ const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme
|
||||
<RefreshControl
|
||||
{...props}
|
||||
enabled={enabled}
|
||||
style={style.container}
|
||||
style={[style.container, isInverted ? style.containerInverse : undefined]}
|
||||
>
|
||||
{children}
|
||||
</RefreshControl>
|
||||
@@ -45,7 +48,7 @@ const PostListRefreshControl = ({children, enabled, onRefresh, refreshing, theme
|
||||
|
||||
return React.cloneElement(
|
||||
children,
|
||||
{refreshControl, inverted: true},
|
||||
{refreshControl, inverted: isInverted},
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ const ViewTypes = keyMirror({
|
||||
|
||||
VIEWING_GLOBAL_THREADS_UNREADS: null,
|
||||
VIEWING_GLOBAL_THREADS_ALL: null,
|
||||
|
||||
THREAD_LAST_VIEWED_AT: null,
|
||||
});
|
||||
|
||||
const RequiredServer = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import {updateThreadLastViewedAt} from '@actions/views/threads';
|
||||
import {Client4} from '@client/rest';
|
||||
import {WebsocketEvents} from '@constants';
|
||||
import {THREAD} from '@constants/screen';
|
||||
@@ -447,7 +448,10 @@ export function setUnreadPost(userId: string, postId: string, location: string)
|
||||
if (isUnreadFromThreadScreen) {
|
||||
const currentTeamId = getThreadTeamId(state, postId);
|
||||
const threadId = post.root_id || post.id;
|
||||
dispatch(handleFollowChanged(threadId, currentTeamId, true));
|
||||
const actions: GenericAction[] = [];
|
||||
actions.push(handleFollowChanged(threadId, currentTeamId, true));
|
||||
actions.push(updateThreadLastViewedAt(threadId, post.create_at));
|
||||
dispatch(batchActions(actions));
|
||||
await dispatch(updateThreadRead(userId, threadId, post.create_at));
|
||||
return {data: true};
|
||||
}
|
||||
|
||||
@@ -215,16 +215,13 @@ export function updateThreadRead(userId: string, threadId: string, timestamp: nu
|
||||
}
|
||||
|
||||
export function handleFollowChanged(threadId: string, teamId: string, following: boolean) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
dispatch({
|
||||
type: ThreadTypes.FOLLOW_CHANGED_THREAD,
|
||||
data: {
|
||||
id: threadId,
|
||||
team_id: teamId,
|
||||
following,
|
||||
},
|
||||
});
|
||||
return {data: true};
|
||||
return {
|
||||
type: ThreadTypes.FOLLOW_CHANGED_THREAD,
|
||||
data: {
|
||||
id: threadId,
|
||||
team_id: teamId,
|
||||
following,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,20 +267,17 @@ export function handleReadChanged(
|
||||
newUnreadReplies: number;
|
||||
},
|
||||
) {
|
||||
return (dispatch: DispatchFunc) => {
|
||||
dispatch({
|
||||
type: ThreadTypes.READ_CHANGED_THREAD,
|
||||
data: {
|
||||
id: threadId,
|
||||
teamId,
|
||||
channelId,
|
||||
lastViewedAt,
|
||||
prevUnreadMentions,
|
||||
newUnreadMentions,
|
||||
prevUnreadReplies,
|
||||
newUnreadReplies,
|
||||
},
|
||||
});
|
||||
return {data: true};
|
||||
return {
|
||||
type: ThreadTypes.READ_CHANGED_THREAD,
|
||||
data: {
|
||||
id: threadId,
|
||||
teamId,
|
||||
channelId,
|
||||
lastViewedAt,
|
||||
prevUnreadMentions,
|
||||
newUnreadMentions,
|
||||
prevUnreadReplies,
|
||||
newUnreadReplies,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ function selectOrderedPostIds(posts: types.posts.Post[], lastViewedAt: number, i
|
||||
lastViewedAt &&
|
||||
post.create_at > lastViewedAt &&
|
||||
!addedNewMessagesIndicator &&
|
||||
indicateNewMessages
|
||||
indicateNewMessages &&
|
||||
currentUser.id !== post.user_id
|
||||
) {
|
||||
out.push(START_OF_NEW_MESSAGES);
|
||||
addedNewMessagesIndicator = true;
|
||||
|
||||
@@ -5,6 +5,17 @@ import {combineReducers} from 'redux';
|
||||
import {ViewTypes} from '@constants';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
|
||||
export const lastViewedAt = (state: Record<string, number> = {}, action: GenericAction) => {
|
||||
switch (action.type) {
|
||||
case ViewTypes.THREAD_LAST_VIEWED_AT:
|
||||
return {
|
||||
...state,
|
||||
[action.data.threadId]: action.data.lastViewedAt,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const viewingGlobalThreads = (state = false, action: GenericAction) => {
|
||||
switch (action.type) {
|
||||
case ViewTypes.VIEWING_GLOBAL_THREADS_SCREEN: {
|
||||
@@ -30,6 +41,7 @@ const viewingGlobalThreadsUnreads = (state = false, action: GenericAction) => {
|
||||
};
|
||||
|
||||
export default combineReducers({
|
||||
lastViewedAt,
|
||||
viewingGlobalThreads,
|
||||
viewingGlobalThreadsUnreads,
|
||||
});
|
||||
|
||||
@@ -27,8 +27,6 @@ exports[`thread should match snapshot, has root post 1`] = `
|
||||
>
|
||||
<Connect(InjectIntl(PostList))
|
||||
currentUserId="member_user_id"
|
||||
indicateNewMessages={false}
|
||||
lastViewedAt={0}
|
||||
location="Thread"
|
||||
postIds={
|
||||
Array [
|
||||
@@ -118,8 +116,6 @@ exports[`thread should match snapshot, no root post, loading 1`] = `
|
||||
exports[`thread should match snapshot, render footer 1`] = `
|
||||
<Connect(InjectIntl(PostList))
|
||||
currentUserId="member_user_id"
|
||||
indicateNewMessages={false}
|
||||
lastViewedAt={0}
|
||||
location="Thread"
|
||||
postIds={
|
||||
Array [
|
||||
@@ -144,8 +140,6 @@ exports[`thread should match snapshot, render footer 1`] = `
|
||||
exports[`thread should match snapshot, render footer 2`] = `
|
||||
<Connect(InjectIntl(PostList))
|
||||
currentUserId="member_user_id"
|
||||
indicateNewMessages={false}
|
||||
lastViewedAt={0}
|
||||
location="Thread"
|
||||
postIds={
|
||||
Array [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators} from 'redux';
|
||||
|
||||
import {updateThreadLastViewedAt} from '@actions/views/threads';
|
||||
import {fetchThreadAppBindings, clearThreadAppBindings} from '@mm-redux/actions/apps';
|
||||
import {selectPost} from '@mm-redux/actions/posts';
|
||||
import {setThreadFollow, updateThreadRead} from '@mm-redux/actions/threads';
|
||||
@@ -12,6 +13,7 @@ import {getCurrentChannelId, getCurrentUserId} from '@mm-redux/selectors/entitie
|
||||
import {makeGetPostIdsForThread} from '@mm-redux/selectors/entities/posts';
|
||||
import {getTheme, isCollapsedThreadsEnabled} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
import {getThreadLastViewedAt} from '@selectors/threads';
|
||||
|
||||
import Thread from './thread';
|
||||
|
||||
@@ -21,6 +23,12 @@ function makeMapStateToProps() {
|
||||
const channel = getChannel(state, ownProps.channelId);
|
||||
|
||||
const collapsedThreadsEnabled = isCollapsedThreadsEnabled(state);
|
||||
const myMember = getMyCurrentChannelMembership(state);
|
||||
const thread = collapsedThreadsEnabled ? getThread(state, ownProps.rootId, true) : null;
|
||||
let lastViewedAt = myMember?.last_viewed_at;
|
||||
if (collapsedThreadsEnabled) {
|
||||
lastViewedAt = getThreadLastViewedAt(state, thread.id);
|
||||
}
|
||||
return {
|
||||
channelId: ownProps.channelId,
|
||||
channelIsArchived: channel ? channel.delete_at !== 0 : false,
|
||||
@@ -28,10 +36,11 @@ function makeMapStateToProps() {
|
||||
collapsedThreadsEnabled,
|
||||
currentUserId: getCurrentUserId(state),
|
||||
displayName: channel ? channel.display_name : '',
|
||||
myMember: getMyCurrentChannelMembership(state),
|
||||
lastViewedAt,
|
||||
myMember,
|
||||
postIds: getPostIdsForThread(state, ownProps.rootId),
|
||||
theme: getTheme(state),
|
||||
thread: collapsedThreadsEnabled ? getThread(state, ownProps.rootId, true) : null,
|
||||
thread,
|
||||
threadLoadingStatus: state.requests.posts.getPostThread,
|
||||
shouldFetchBindings: ownProps.channelId !== getCurrentChannelId(state),
|
||||
};
|
||||
@@ -44,6 +53,7 @@ function mapDispatchToProps(dispatch) {
|
||||
selectPost,
|
||||
setThreadFollow,
|
||||
updateThreadRead,
|
||||
updateThreadLastViewedAt,
|
||||
fetchThreadAppBindings,
|
||||
clearThreadAppBindings,
|
||||
}, dispatch),
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class ThreadAndroid extends ThreadBase {
|
||||
postIds,
|
||||
rootId,
|
||||
channelIsArchived,
|
||||
collapsedThreadsEnabled,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
@@ -38,7 +39,7 @@ export default class ThreadAndroid extends ThreadBase {
|
||||
<PostList
|
||||
testID='thread.post_list'
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
indicateNewMessages={collapsedThreadsEnabled}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
|
||||
@@ -34,6 +34,7 @@ export default class ThreadIOS extends ThreadBase {
|
||||
postIds,
|
||||
rootId,
|
||||
channelIsArchived,
|
||||
collapsedThreadsEnabled,
|
||||
theme,
|
||||
} = this.props;
|
||||
|
||||
@@ -49,7 +50,7 @@ export default class ThreadIOS extends ThreadBase {
|
||||
<PostList
|
||||
testID='thread.post_list'
|
||||
renderFooter={this.renderFooter()}
|
||||
indicateNewMessages={false}
|
||||
indicateNewMessages={collapsedThreadsEnabled}
|
||||
postIds={postIds}
|
||||
currentUserId={myMember && myMember.user_id}
|
||||
lastViewedAt={this.state.lastViewedAt}
|
||||
|
||||
@@ -19,6 +19,7 @@ export default class ThreadBase extends PureComponent {
|
||||
actions: PropTypes.shape({
|
||||
selectPost: PropTypes.func.isRequired,
|
||||
setThreadFollow: PropTypes.func.isRequired,
|
||||
updateThreadLastViewedAt: PropTypes.func,
|
||||
updateThreadRead: PropTypes.func,
|
||||
fetchThreadAppBindings: PropTypes.func.isRequired,
|
||||
clearThreadAppBindings: PropTypes.func.isRequired,
|
||||
@@ -29,6 +30,7 @@ export default class ThreadBase extends PureComponent {
|
||||
collapsedThreadsEnabled: PropTypes.bool,
|
||||
currentUserId: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
lastViewedAt: PropTypes.number,
|
||||
myMember: PropTypes.object.isRequired,
|
||||
postIds: PropTypes.array.isRequired,
|
||||
rootId: PropTypes.string.isRequired,
|
||||
@@ -109,7 +111,7 @@ export default class ThreadBase extends PureComponent {
|
||||
this.postDraft = React.createRef();
|
||||
|
||||
this.state = {
|
||||
lastViewedAt: props.myMember && props.myMember.last_viewed_at,
|
||||
lastViewedAt: props.lastViewedAt,
|
||||
};
|
||||
|
||||
this.bottomPadding = new Animated.Value(0);
|
||||
@@ -131,8 +133,13 @@ export default class ThreadBase extends PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.lastViewedAt) {
|
||||
this.setState({lastViewedAt: nextProps.myMember && nextProps.myMember.last_viewed_at});
|
||||
if (
|
||||
(!this.props.collapsedThreadsEnabled && !this.state.lastViewedAt) ||
|
||||
(this.props.collapsedThreadsEnabled && this.props.lastViewedAt !== nextProps.lastViewedAt)
|
||||
) {
|
||||
this.setState({
|
||||
lastViewedAt: nextProps.lastViewedAt,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.postIds.length < nextProps.postIds.length) {
|
||||
@@ -158,23 +165,33 @@ export default class ThreadBase extends PureComponent {
|
||||
this.props.actions.setThreadFollow(currentUserId, rootId, !thread?.is_following);
|
||||
}
|
||||
|
||||
hasUnreadPost() {
|
||||
return Boolean(
|
||||
this.props.thread.last_viewed_at < this.props.thread.last_reply_at ||
|
||||
this.props.thread.unread_mentions ||
|
||||
this.props.thread.unread_replies,
|
||||
);
|
||||
}
|
||||
|
||||
markThreadRead(hasNewPost = false) {
|
||||
if (
|
||||
this.props.collapsedThreadsEnabled &&
|
||||
this.props.thread &&
|
||||
this.props.thread.is_following &&
|
||||
(
|
||||
hasNewPost ||
|
||||
this.props.thread.last_viewed_at < this.props.thread.last_reply_at ||
|
||||
this.props.thread.unread_mentions ||
|
||||
this.props.thread.unread_replies
|
||||
)
|
||||
) {
|
||||
this.props.actions.updateThreadRead(
|
||||
this.props.currentUserId,
|
||||
this.props.rootId,
|
||||
Date.now(),
|
||||
);
|
||||
const {thread} = this.props;
|
||||
|
||||
if (this.props.collapsedThreadsEnabled && thread?.is_following) {
|
||||
// Update lastViewedAt on marking thread as read on openining the screen.
|
||||
if (!hasNewPost) {
|
||||
this.props.actions.updateThreadLastViewedAt(
|
||||
thread.id,
|
||||
thread.last_viewed_at,
|
||||
);
|
||||
}
|
||||
|
||||
if (hasNewPost || this.hasUnreadPost()) {
|
||||
this.props.actions.updateThreadRead(
|
||||
this.props.currentUserId,
|
||||
this.props.rootId,
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +237,7 @@ export default class ThreadBase extends PureComponent {
|
||||
}
|
||||
|
||||
bottomPaddingAnimation = (visible) => {
|
||||
const [padding, duration] = visible ?
|
||||
[TYPING_HEIGHT, 200] :
|
||||
[0, 400];
|
||||
const [padding, duration] = visible ? [TYPING_HEIGHT, 200] : [0, 400];
|
||||
|
||||
return Animated.timing(this.bottomPadding, {
|
||||
toValue: padding,
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {getThread} from '@mm-redux/selectors/entities/threads';
|
||||
|
||||
import type {GlobalState} from '@mm-redux/types/store';
|
||||
import type {UserThread} from '@mm-redux/types/threads';
|
||||
import type {$ID} from '@mm-redux/types/utilities';
|
||||
|
||||
export function getThreadLastViewedAt(state: GlobalState, threadId: $ID<UserThread>): number {
|
||||
if (state.views.threads.lastViewedAt[threadId]) {
|
||||
// timestamp - 1, to properly mark "new messages" in threads when manually unread as lastViewedAt is set to the post's created timestamp
|
||||
return state.views.threads.lastViewedAt[threadId] - 1;
|
||||
}
|
||||
return getThread(state, threadId)?.last_viewed_at || 0;
|
||||
}
|
||||
|
||||
export function getViewingGlobalThreads(state: GlobalState): boolean {
|
||||
return state.views.threads.viewingGlobalThreads;
|
||||
|
||||
@@ -153,6 +153,7 @@ const state = {
|
||||
lastTeamId: '',
|
||||
},
|
||||
threads: {
|
||||
lastViewedAt: {},
|
||||
viewingGlobalThreads: false,
|
||||
viewingGlobalThreadsUnreads: false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user