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:
Anurag Shivarathri
2021-10-07 15:14:34 +05:30
committed by GitHub
parent 1a35250811
commit a81b56212e
19 changed files with 300 additions and 149 deletions

View File

@@ -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,

View File

@@ -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));

View File

@@ -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}

View File

@@ -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>
`;

View File

@@ -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'],

View File

@@ -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>
);
}

View File

@@ -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},
);
};

View File

@@ -105,6 +105,8 @@ const ViewTypes = keyMirror({
VIEWING_GLOBAL_THREADS_UNREADS: null,
VIEWING_GLOBAL_THREADS_ALL: null,
THREAD_LAST_VIEWED_AT: null,
});
const RequiredServer = {

View File

@@ -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};
}

View File

@@ -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,
},
};
}

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -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 [

View File

@@ -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),

View File

@@ -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}

View File

@@ -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}

View File

@@ -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,

View File

@@ -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;

View File

@@ -153,6 +153,7 @@ const state = {
lastTeamId: '',
},
threads: {
lastViewedAt: {},
viewingGlobalThreads: false,
viewingGlobalThreadsUnreads: false,
},