[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);
|
const thread = getThread(state, post.root_id);
|
||||||
|
|
||||||
// mark a thread as read (when the user is viewing the thread)
|
// 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)
|
// update the new messages line (when there are no previous unreads)
|
||||||
if (thread.last_reply_at < getThreadLastViewedAt(state, thread.id)) {
|
if (thread.last_reply_at < getThreadLastViewedAt(state, thread.id)) {
|
||||||
dispatch(updateThreadLastOpened(thread.id, post.create_at));
|
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 * as Actions from 'actions/views/channel';
|
||||||
import {closeRightHandSide} from 'actions/views/rhs';
|
import {closeRightHandSide} from 'actions/views/rhs';
|
||||||
|
import {markThreadAsRead} from 'actions/views/threads';
|
||||||
|
|
||||||
import mockStore from 'tests/test_store';
|
import mockStore from 'tests/test_store';
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
@@ -39,6 +40,11 @@ jest.mock('actions/views/rhs', () => ({
|
|||||||
closeRightHandSide: jest.fn(() => ({type: ''})),
|
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('mattermost-redux/actions/posts');
|
||||||
|
|
||||||
jest.mock('selectors/local_storage', () => ({
|
jest.mock('selectors/local_storage', () => ({
|
||||||
@@ -110,6 +116,7 @@ describe('channel view actions', () => {
|
|||||||
rhs: {
|
rhs: {
|
||||||
selectedPostId: '',
|
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 () => {
|
test('should mark channel as read when channel is not manually unread', async () => {
|
||||||
store = mockStore(initialState);
|
store = mockStore(initialState);
|
||||||
|
|
||||||
await store.dispatch(Actions.markChannelAsReadOnFocus(channel1.id));
|
await store.dispatch(Actions.markAsReadOnFocus());
|
||||||
|
|
||||||
expect(markChannelAsRead).toHaveBeenCalledWith(channel1.id);
|
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();
|
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 () => {
|
test('should match actions for PREFETCH_POSTS_FOR_CHANNEL when prefetch argument and getPostsSince sucess', async () => {
|
||||||
const channelId = 'channel1';
|
const channelId = 'channel1';
|
||||||
PostActions.getPostsSince.mockReturnValue(() => ({data: []}));
|
PostActions.getPostsSince.mockReturnValue(() => ({data: []}));
|
||||||
|
|||||||
@@ -49,9 +49,11 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
|||||||
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
import {openDirectChannelToUserId} from 'actions/channel_actions';
|
||||||
import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions';
|
import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions';
|
||||||
import {closeRightHandSide} from 'actions/views/rhs';
|
import {closeRightHandSide} from 'actions/views/rhs';
|
||||||
|
import {markThreadAsRead} from 'actions/views/threads';
|
||||||
import {getLastViewedChannelName} from 'selectors/local_storage';
|
import {getLastViewedChannelName} from 'selectors/local_storage';
|
||||||
import {getSelectedPost, getSelectedPostId} from 'selectors/rhs';
|
import {getSelectedPost, getSelectedPostId} from 'selectors/rhs';
|
||||||
import {getLastPostsApiTimeForChannel} from 'selectors/views/channel';
|
import {getLastPostsApiTimeForChannel} from 'selectors/views/channel';
|
||||||
|
import {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads';
|
||||||
import {getSocketStatus} from 'selectors/views/websocket';
|
import {getSocketStatus} from 'selectors/views/websocket';
|
||||||
import LocalStorageStore from 'stores/local_storage_store';
|
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) => {
|
return (dispatch, getState) => {
|
||||||
if (isManuallyUnread(getState(), channelId)) {
|
const state = getState();
|
||||||
return;
|
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 {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 {ActionTypes, Threads} from 'utils/constants';
|
||||||
|
|
||||||
|
import type {GlobalState} from 'types/store';
|
||||||
|
|
||||||
export function updateThreadLastOpened(threadId: string, lastViewedAt: number) {
|
export function updateThreadLastOpened(threadId: string, lastViewedAt: number) {
|
||||||
return {
|
return {
|
||||||
type: Threads.CHANGED_LAST_VIEWED_AT,
|
type: Threads.CHANGED_LAST_VIEWED_AT,
|
||||||
@@ -41,3 +50,16 @@ export function updateThreadToastStatus(status: boolean) {
|
|||||||
data: status,
|
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;
|
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();
|
lastViewedAt = Date.now();
|
||||||
|
|
||||||
// Sometimes `Date.now()` was generating a timestamp before the
|
// 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 {getCurrentTeamId, getMyTeams} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
|
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 {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads';
|
||||||
|
|
||||||
import {initializeTeam, joinTeam} from 'components/team_controller/actions';
|
import {initializeTeam, joinTeam} from 'components/team_controller/actions';
|
||||||
@@ -43,6 +44,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
teamsList: getMyTeams(state),
|
teamsList: getMyTeams(state),
|
||||||
plugins,
|
plugins,
|
||||||
selectedThreadId: getSelectedThreadIdInCurrentTeam(state),
|
selectedThreadId: getSelectedThreadIdInCurrentTeam(state),
|
||||||
|
selectedPostId: getSelectedPostId(state),
|
||||||
mfaRequired: checkIfMFARequired(currentUser, license, config, ownProps.match.url),
|
mfaRequired: checkIfMFARequired(currentUser, license, config, ownProps.match.url),
|
||||||
disableRefetchingOnBrowserFocus,
|
disableRefetchingOnBrowserFocus,
|
||||||
disableWakeUpReconnectHandler,
|
disableWakeUpReconnectHandler,
|
||||||
@@ -52,7 +54,7 @@ function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchChannelsAndMembers,
|
fetchChannelsAndMembers,
|
||||||
fetchAllMyTeamsChannelsAndChannelMembersREST,
|
fetchAllMyTeamsChannelsAndChannelMembersREST,
|
||||||
markChannelAsReadOnFocus,
|
markAsReadOnFocus,
|
||||||
initializeTeam,
|
initializeTeam,
|
||||||
joinTeam,
|
joinTeam,
|
||||||
unsetActiveChannelOnServer,
|
unsetActiveChannelOnServer,
|
||||||
|
|||||||
@@ -84,13 +84,8 @@ function TeamController(props: Props) {
|
|||||||
// Effect runs on mount, add event listeners on windows object
|
// Effect runs on mount, add event listeners on windows object
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleFocus() {
|
function handleFocus() {
|
||||||
if (props.selectedThreadId) {
|
window.isActive = true;
|
||||||
window.isActive = true;
|
props.markAsReadOnFocus();
|
||||||
}
|
|
||||||
if (props.currentChannelId) {
|
|
||||||
window.isActive = true;
|
|
||||||
props.markChannelAsReadOnFocus(props.currentChannelId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary flag to disable refetching of channel members on browser focus
|
// Temporary flag to disable refetching of channel members on browser focus
|
||||||
if (!props.disableRefetchingOnBrowserFocus) {
|
if (!props.disableRefetchingOnBrowserFocus) {
|
||||||
@@ -130,7 +125,7 @@ function TeamController(props: Props) {
|
|||||||
window.removeEventListener('blur', handleBlur);
|
window.removeEventListener('blur', handleBlur);
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
};
|
};
|
||||||
}, [props.selectedThreadId, props.currentChannelId, props.currentTeamId]);
|
}, [props.currentTeamId]);
|
||||||
|
|
||||||
// Effect runs on mount, adds active state to window
|
// Effect runs on mount, adds active state to window
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user