[MM-58567] Added window.isActive check for marking threads as active when they're open, mark the threads as read when the window becomes active again (#27988)
* [MM-58567] Added `window.isActive` check for marking threads as active when they're open, mark the threads as read when the window becomes active again * PR feedback
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
5482792697
Коммит
18ffb656ab
@@ -148,7 +148,7 @@ export function setThreadRead(post: Post): ActionFunc<boolean, GlobalState> {
|
||||
const thread = getThread(state, post.root_id);
|
||||
|
||||
// mark a thread as read (when the user is viewing the thread)
|
||||
if (thread && isThreadOpen(state, thread.id)) {
|
||||
if (thread && isThreadOpen(state, thread.id) && window.isActive) {
|
||||
// update the new messages line (when there are no previous unreads)
|
||||
if (thread.last_reply_at < getThreadLastViewedAt(state, thread.id)) {
|
||||
dispatch(updateThreadLastOpened(thread.id, post.create_at));
|
||||
|
||||
@@ -8,6 +8,7 @@ import {General, Posts, RequestStatus} from 'mattermost-redux/constants';
|
||||
|
||||
import * as Actions from 'actions/views/channel';
|
||||
import {closeRightHandSide} from 'actions/views/rhs';
|
||||
import {markThreadAsRead} from 'actions/views/threads';
|
||||
|
||||
import mockStore from 'tests/test_store';
|
||||
import {getHistory} from 'utils/browser_history';
|
||||
@@ -39,6 +40,11 @@ jest.mock('actions/views/rhs', () => ({
|
||||
closeRightHandSide: jest.fn(() => ({type: ''})),
|
||||
}));
|
||||
|
||||
jest.mock('actions/views/threads', () => ({
|
||||
...jest.requireActual('actions/views/threads'),
|
||||
markThreadAsRead: jest.fn(() => ({type: ''})),
|
||||
}));
|
||||
|
||||
jest.mock('mattermost-redux/actions/posts');
|
||||
|
||||
jest.mock('selectors/local_storage', () => ({
|
||||
@@ -110,6 +116,7 @@ describe('channel view actions', () => {
|
||||
rhs: {
|
||||
selectedPostId: '',
|
||||
},
|
||||
threads: {},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -589,11 +596,11 @@ describe('channel view actions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('markChannelAsReadOnFocus', () => {
|
||||
describe('markAsReadOnFocus', () => {
|
||||
test('should mark channel as read when channel is not manually unread', async () => {
|
||||
store = mockStore(initialState);
|
||||
|
||||
await store.dispatch(Actions.markChannelAsReadOnFocus(channel1.id));
|
||||
await store.dispatch(Actions.markAsReadOnFocus());
|
||||
|
||||
expect(markChannelAsRead).toHaveBeenCalledWith(channel1.id);
|
||||
});
|
||||
@@ -612,11 +619,36 @@ describe('channel view actions', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(Actions.markChannelAsReadOnFocus(channel1.id));
|
||||
await store.dispatch(Actions.markAsReadOnFocus());
|
||||
|
||||
expect(markChannelAsRead).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('should dispatch markThreadAsRead when threads are selected', async () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
views: {
|
||||
...initialState.views,
|
||||
rhs: {
|
||||
...initialState.views.rhs,
|
||||
selectedPostId: 'post_id',
|
||||
},
|
||||
threads: {
|
||||
...initialState.views.threads,
|
||||
selectedThreadIdInTeam: {
|
||||
teamid1: 'thread_id',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await store.dispatch(Actions.markAsReadOnFocus());
|
||||
|
||||
expect(markThreadAsRead).toHaveBeenCalledTimes(2);
|
||||
expect(markThreadAsRead).toHaveBeenCalledWith('thread_id');
|
||||
expect(markThreadAsRead).toHaveBeenCalledWith('post_id');
|
||||
});
|
||||
|
||||
test('should match actions for PREFETCH_POSTS_FOR_CHANNEL when prefetch argument and getPostsSince sucess', async () => {
|
||||
const channelId = 'channel1';
|
||||
PostActions.getPostsSince.mockReturnValue(() => ({data: []}));
|
||||
|
||||
@@ -49,9 +49,11 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
||||
import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions';
|
||||
import {closeRightHandSide} from 'actions/views/rhs';
|
||||
import {markThreadAsRead} from 'actions/views/threads';
|
||||
import {getLastViewedChannelName} from 'selectors/local_storage';
|
||||
import {getSelectedPost, getSelectedPostId} from 'selectors/rhs';
|
||||
import {getLastPostsApiTimeForChannel} from 'selectors/views/channel';
|
||||
import {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads';
|
||||
import {getSocketStatus} from 'selectors/views/websocket';
|
||||
import LocalStorageStore from 'stores/local_storage_store';
|
||||
|
||||
@@ -493,13 +495,24 @@ export function scrollPostListToBottom() {
|
||||
};
|
||||
}
|
||||
|
||||
export function markChannelAsReadOnFocus(channelId: string): ThunkActionFunc<void> {
|
||||
export function markAsReadOnFocus(): ThunkActionFunc<void, GlobalState> {
|
||||
return (dispatch, getState) => {
|
||||
if (isManuallyUnread(getState(), channelId)) {
|
||||
return;
|
||||
const state = getState();
|
||||
const currentChannelId = getCurrentChannelId(state);
|
||||
const selectedThreadId = getSelectedThreadIdInCurrentTeam(state);
|
||||
const selectedPostId = getSelectedPostId(state);
|
||||
|
||||
if (!isManuallyUnread(getState(), currentChannelId)) {
|
||||
dispatch(markChannelAsRead(currentChannelId));
|
||||
}
|
||||
|
||||
dispatch(markChannelAsRead(channelId));
|
||||
if (selectedThreadId) {
|
||||
dispatch(markThreadAsRead(selectedThreadId));
|
||||
}
|
||||
|
||||
if (currentChannelId && selectedPostId) {
|
||||
dispatch(markThreadAsRead(selectedPostId));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,17 @@
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
|
||||
import {updateThreadRead} from 'mattermost-redux/actions/threads';
|
||||
import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
import type {ThunkActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import {isThreadManuallyUnread, isThreadOpen} from 'selectors/views/threads';
|
||||
|
||||
import {ActionTypes, Threads} from 'utils/constants';
|
||||
|
||||
import type {GlobalState} from 'types/store';
|
||||
|
||||
export function updateThreadLastOpened(threadId: string, lastViewedAt: number) {
|
||||
return {
|
||||
type: Threads.CHANGED_LAST_VIEWED_AT,
|
||||
@@ -41,3 +50,16 @@ export function updateThreadToastStatus(status: boolean) {
|
||||
data: status,
|
||||
};
|
||||
}
|
||||
|
||||
export function markThreadAsRead(threadId: string): ThunkActionFunc<void, GlobalState> {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const currentUserId = getCurrentUserId(state);
|
||||
const currentTeamId = getCurrentTeamId(state);
|
||||
|
||||
if (isThreadOpen(state, threadId) && window.isActive && !isThreadManuallyUnread(state, threadId)) {
|
||||
// mark thread as read on the server
|
||||
dispatch(updateThreadRead(currentUserId, currentTeamId, threadId, Date.now()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1680,7 +1680,7 @@ function handleThreadUpdated(msg) {
|
||||
threadData.is_following = true;
|
||||
}
|
||||
|
||||
if (isThreadOpen(state, threadData.id) && !isThreadManuallyUnread(state, threadData.id)) {
|
||||
if (isThreadOpen(state, threadData.id) && window.isActive && !isThreadManuallyUnread(state, threadData.id)) {
|
||||
lastViewedAt = Date.now();
|
||||
|
||||
// Sometimes `Date.now()` was generating a timestamp before the
|
||||
|
||||
@@ -11,7 +11,8 @@ import {getLicense, getConfig} from 'mattermost-redux/selectors/entities/general
|
||||
import {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {markChannelAsReadOnFocus} from 'actions/views/channel';
|
||||
import {markAsReadOnFocus} from 'actions/views/channel';
|
||||
import {getSelectedPostId} from 'selectors/rhs';
|
||||
import {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads';
|
||||
|
||||
import {initializeTeam, joinTeam} from 'components/team_controller/actions';
|
||||
@@ -43,6 +44,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
teamsList: getMyTeams(state),
|
||||
plugins,
|
||||
selectedThreadId: getSelectedThreadIdInCurrentTeam(state),
|
||||
selectedPostId: getSelectedPostId(state),
|
||||
mfaRequired: checkIfMFARequired(currentUser, license, config, ownProps.match.url),
|
||||
disableRefetchingOnBrowserFocus,
|
||||
disableWakeUpReconnectHandler,
|
||||
@@ -52,7 +54,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
||||
const mapDispatchToProps = {
|
||||
fetchChannelsAndMembers,
|
||||
fetchAllMyTeamsChannelsAndChannelMembersREST,
|
||||
markChannelAsReadOnFocus,
|
||||
markAsReadOnFocus,
|
||||
initializeTeam,
|
||||
joinTeam,
|
||||
unsetActiveChannelOnServer,
|
||||
|
||||
@@ -84,13 +84,8 @@ function TeamController(props: Props) {
|
||||
// Effect runs on mount, add event listeners on windows object
|
||||
useEffect(() => {
|
||||
function handleFocus() {
|
||||
if (props.selectedThreadId) {
|
||||
window.isActive = true;
|
||||
}
|
||||
if (props.currentChannelId) {
|
||||
window.isActive = true;
|
||||
props.markChannelAsReadOnFocus(props.currentChannelId);
|
||||
}
|
||||
window.isActive = true;
|
||||
props.markAsReadOnFocus();
|
||||
|
||||
// Temporary flag to disable refetching of channel members on browser focus
|
||||
if (!props.disableRefetchingOnBrowserFocus) {
|
||||
@@ -130,7 +125,7 @@ function TeamController(props: Props) {
|
||||
window.removeEventListener('blur', handleBlur);
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, [props.selectedThreadId, props.currentChannelId, props.currentTeamId]);
|
||||
}, [props.currentTeamId]);
|
||||
|
||||
// Effect runs on mount, adds active state to window
|
||||
useEffect(() => {
|
||||
|
||||
Ссылка в новой задаче
Block a user