[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
Этот коммит содержится в:
Devin Binnie
2024-08-22 11:35:33 -04:00
коммит произвёл GitHub
родитель 5482792697
Коммит 18ffb656ab
7 изменённых файлов: 83 добавлений и 19 удалений

Просмотреть файл

@@ -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(() => {