diff --git a/lib/handlers/dm-activity-handler.js b/lib/handlers/dm-activity-handler.js index 25c06a5e8..a749f7df4 100644 --- a/lib/handlers/dm-activity-handler.js +++ b/lib/handlers/dm-activity-handler.js @@ -1,107 +1,102 @@ // @flow + import invariant from 'invariant'; import * as React from 'react'; -import { updateActivityActionTypes } from '../actions/activity-actions.js'; import { type OutboundDMOperationSpecification, dmOperationSpecificationTypes, } from '../shared/dm-ops/dm-op-utils.js'; import { useProcessAndSendDMOperation } from '../shared/dm-ops/process-dm-ops.js'; import { getMostRecentNonLocalMessageID } from '../shared/message-utils.js'; import { threadIsPending } from '../shared/thread-utils.js'; -import type { ActivityUpdateSuccessPayload } from '../types/activity-types.js'; import type { DMChangeThreadReadStatusOperation } from '../types/dm-ops.js'; import type { RawThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; import { threadTypeIsThick } from '../types/thread-types-enum.js'; import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; import { useSelector } from '../utils/redux-utils.js'; function useUpdateDMActivity(): ( viewerID: string, activeThreadInfo: RawThreadInfo, -) => Promise { +) => Promise { const processAndSendDMOperation = useProcessAndSendDMOperation(); return React.useCallback( async (viewerID: string, activeThreadInfo: RawThreadInfo) => { invariant( threadTypeIsThick(activeThreadInfo.type), 'thread must be thick', ); const op: DMChangeThreadReadStatusOperation = { type: 'change_thread_read_status', time: Date.now(), threadID: activeThreadInfo.id, creatorID: viewerID, unread: false, }; const opSpecification: OutboundDMOperationSpecification = { type: dmOperationSpecificationTypes.OUTBOUND, op, recipients: { type: 'self_devices' }, }; await processAndSendDMOperation(opSpecification); - return { activityUpdates: {}, result: { unfocusedToUnread: [] } }; }, [processAndSendDMOperation], ); } function useDMActivityHandler(activeThread: ?string): void { const activeThreadInfo = useSelector(state => activeThread ? state.threadStore.threadInfos[activeThread] : null, ); const activeThreadLatestMessage = useSelector(state => activeThread ? getMostRecentNonLocalMessageID(activeThread, state.messageStore) : null, ); const processAndSendDMOperation = useProcessAndSendDMOperation(); const prevActiveThreadRef = React.useRef(); const prevActiveThreadLatestMessageRef = React.useRef(); const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const updateDMActivity = useUpdateDMActivity(); const dispatchActionPromise = useDispatchActionPromise(); React.useEffect(() => { const prevActiveThread = prevActiveThreadRef.current; const prevActiveThreadLatestMessage = prevActiveThreadLatestMessageRef.current; prevActiveThreadRef.current = activeThread; prevActiveThreadLatestMessageRef.current = activeThreadLatestMessage; if ( !viewerID || !activeThread || !activeThreadInfo || !threadTypeIsThick(activeThreadInfo.type) || threadIsPending(activeThread) || (activeThread === prevActiveThread && activeThreadLatestMessage === prevActiveThreadLatestMessage) ) { return; } - void dispatchActionPromise( - updateActivityActionTypes, - updateDMActivity(viewerID, activeThreadInfo), - ); + void updateDMActivity(viewerID, activeThreadInfo); }, [ updateDMActivity, dispatchActionPromise, activeThread, viewerID, processAndSendDMOperation, activeThreadInfo, activeThreadLatestMessage, ]); } export default useDMActivityHandler; diff --git a/native/components/dm-activity-handler.react.js b/native/components/dm-activity-handler.react.js index 14e69ae48..609d31dd1 100644 --- a/native/components/dm-activity-handler.react.js +++ b/native/components/dm-activity-handler.react.js @@ -1,27 +1,15 @@ // @flow + import * as React from 'react'; import useDMActivityHandler from 'lib/handlers/dm-activity-handler.js'; -import { isLoggedIn } from 'lib/selectors/user-selectors.js'; -import { activeMessageListSelector } from '../navigation/nav-selectors.js'; -import { NavContext } from '../navigation/navigation-context.js'; -import { useSelector } from '../redux/redux-utils.js'; +import { useForegroundActiveThread } from '../navigation/nav-selectors.js'; function DMActivityHandler(): React.Node { - const active = useSelector( - state => isLoggedIn(state) && state.lifecycleState !== 'background', - ); - const navContext = React.useContext(NavContext); - const activeThread = React.useMemo(() => { - if (!active) { - return null; - } - return activeMessageListSelector(navContext); - }, [active, navContext]); - + const activeThread = useForegroundActiveThread(); useDMActivityHandler(activeThread); return null; } export default DMActivityHandler; diff --git a/native/navigation/nav-selectors.js b/native/navigation/nav-selectors.js index 7df61edab..28b8b6782 100644 --- a/native/navigation/nav-selectors.js +++ b/native/navigation/nav-selectors.js @@ -1,445 +1,460 @@ // @flow import type { PossiblyStaleNavigationState } from '@react-navigation/core'; import { useRoute } from '@react-navigation/native'; import _memoize from 'lodash/memoize.js'; import * as React from 'react'; import { createSelector } from 'reselect'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js'; +import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useCanEditMessage } from 'lib/shared/edit-messages-utils.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { CalendarFilter } from 'lib/types/filter-types.js'; import type { ComposableMessageInfo, RobotextMessageInfo, } from 'lib/types/message-types.js'; import type { ThreadInfo } from 'lib/types/minimally-encoded-thread-permissions-types.js'; import type { GlobalTheme } from 'lib/types/theme-types.js'; import type { NavContextType } from './navigation-context.js'; import { NavContext } from './navigation-context.js'; import { currentLeafRoute, getStateFromNavigatorRoute, getThreadIDFromRoute, } from './navigation-utils.js'; import { accountModals, ActionResultModalRouteName, AppRouteName, CalendarRouteName, chatRootModals, ChatRouteName, CommunityDrawerNavigatorRouteName, MessageListRouteName, PinnedMessagesScreenRouteName, MessageSearchRouteName, scrollBlockingModals, TabNavigatorRouteName, ThreadPickerModalRouteName, threadRoutes, } from './route-names.js'; import type { RemoveEditMode } from '../chat/message-list-types.js'; import { useSelector } from '../redux/redux-utils.js'; import type { NavPlusRedux } from '../types/selector-types.js'; const baseCreateIsForegroundSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = (routeName: string) => createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } return navigationState.routes[navigationState.index].name === routeName; }, ); const createIsForegroundSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = _memoize( baseCreateIsForegroundSelector, ); function useIsAppLoggedIn(): boolean { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return false; } const { state } = navContext; return !accountModals.includes(state.routes[state.index].name); }, [navContext]); } const baseCreateActiveTabSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = (routeName: string) => createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { return false; } const appState = getStateFromNavigatorRoute(currentRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return false; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return false; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); return tabState.routes[tabState.index].name === routeName; }, ); const createActiveTabSelector: ( routeName: string, ) => (context: ?NavContextType) => boolean = _memoize( baseCreateActiveTabSelector, ); const scrollBlockingModalsClosedSelector: ( context: ?NavContextType, ) => boolean = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { return true; } const appState = getStateFromNavigatorRoute(currentRootSubroute); for (let i = appState.index; i >= 0; i--) { const route = appState.routes[i]; if (scrollBlockingModals.includes(route.name)) { return false; } } return true; }, ); function selectBackgroundIsDark( navigationState: ?PossiblyStaleNavigationState, theme: ?GlobalTheme, ): boolean { if (!navigationState) { return false; } const currentRootSubroute = navigationState.routes[navigationState.index]; if (currentRootSubroute.name !== AppRouteName) { // Very bright... we'll call it non-dark. Doesn't matter right now since // we only use this selector for determining ActionResultModal appearance return false; } const appState = getStateFromNavigatorRoute(currentRootSubroute); let appIndex = appState.index; let currentAppSubroute = appState.routes[appIndex]; while (currentAppSubroute.name === ActionResultModalRouteName) { currentAppSubroute = appState.routes[--appIndex]; } if (scrollBlockingModals.includes(currentAppSubroute.name)) { // All the scroll-blocking chat modals have a dark background return true; } return theme === 'dark'; } function activeThread( navigationState: ?PossiblyStaleNavigationState, validRouteNames: $ReadOnlyArray, ): ?string { if (!navigationState) { return null; } let rootIndex = navigationState.index; let currentRootSubroute = navigationState.routes[rootIndex]; while (currentRootSubroute.name !== AppRouteName) { if (!chatRootModals.includes(currentRootSubroute.name)) { return null; } if (rootIndex === 0) { return null; } currentRootSubroute = navigationState.routes[--rootIndex]; } const appState = getStateFromNavigatorRoute(currentRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return null; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return null; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); const currentTabSubroute = tabState.routes[tabState.index]; if (currentTabSubroute.name !== ChatRouteName) { return null; } const chatState = getStateFromNavigatorRoute(currentTabSubroute); const currentChatSubroute = chatState.routes[chatState.index]; return getThreadIDFromRoute(currentChatSubroute, validRouteNames); } const activeThreadSelector: (context: ?NavContextType) => ?string = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState): ?string => activeThread(navigationState, threadRoutes), ); const messageListRouteNames = [MessageListRouteName]; const activeMessageListSelector: (context: ?NavContextType) => ?string = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState): ?string => activeThread(navigationState, messageListRouteNames), ); +function useForegroundActiveThread(): ?string { + const active = useSelector( + state => isLoggedIn(state) && state.lifecycleState !== 'background', + ); + const navContext = React.useContext(NavContext); + return React.useMemo(() => { + if (!active) { + return null; + } + return activeMessageListSelector(navContext); + }, [active, navContext]); +} + function useActiveThread(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return null; } const { state } = navContext; return activeThread(state, threadRoutes); }, [navContext]); } function useActiveMessageList(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return null; } const { state } = navContext; return activeThread(state, messageListRouteNames); }, [navContext]); } const calendarTabActiveSelector = createActiveTabSelector(CalendarRouteName); const threadPickerActiveSelector = createIsForegroundSelector( ThreadPickerModalRouteName, ); const calendarActiveSelector: (context: ?NavContextType) => boolean = createSelector( calendarTabActiveSelector, threadPickerActiveSelector, (calendarTabActive: boolean, threadPickerActive: boolean) => calendarTabActive || threadPickerActive, ); const nativeCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery = createSelector( (input: NavPlusRedux) => currentCalendarQuery(input.redux), (input: NavPlusRedux) => calendarActiveSelector(input.navContext), ( calendarQuery: (calendarActive: boolean) => CalendarQuery, calendarActive: boolean, ) => () => calendarQuery(calendarActive), ); const nonThreadCalendarQuery: (input: NavPlusRedux) => () => CalendarQuery = createSelector( nativeCalendarQuery, (input: NavPlusRedux) => nonThreadCalendarFiltersSelector(input.redux), ( calendarQuery: () => CalendarQuery, filters: $ReadOnlyArray, ) => { return (): CalendarQuery => { const query = calendarQuery(); return { startDate: query.startDate, endDate: query.endDate, filters, }; }; }, ); function useCalendarQuery(): () => CalendarQuery { const navContext = React.useContext(NavContext); return useSelector(state => nonThreadCalendarQuery({ redux: state, navContext, }), ); } const drawerSwipeEnabledSelector: (context: ?NavContextType) => boolean = createSelector( (context: ?NavContextType) => context && context.state, (navigationState: ?PossiblyStaleNavigationState) => { if (!navigationState) { return true; } // First, we recurse into the navigation state until we find the tab route // The tab route should always be accessible by recursing through the // first routes of each subsequent nested navigation state const [firstRootSubroute] = navigationState.routes; if (firstRootSubroute.name !== AppRouteName) { return true; } const appState = getStateFromNavigatorRoute(firstRootSubroute); const [firstAppSubroute] = appState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return true; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return true; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); // Once we have the tab state, we want to figure out if we currently have // an active StackNavigator const currentTabSubroute = tabState.routes[tabState.index]; if (!currentTabSubroute.state) { return true; } const currentTabSubrouteState = getStateFromNavigatorRoute(currentTabSubroute); if (currentTabSubrouteState.type !== 'stack') { return true; } // Finally, we want to disable the swipe gesture if there is a stack with // more than one subroute, since then the stack will have its own swipe // gesture that will conflict with the drawer's return currentTabSubrouteState.routes.length < 2; }, ); function getTabNavState( navigationState: ?PossiblyStaleNavigationState, ): ?PossiblyStaleNavigationState { if (!navigationState) { return null; } const [firstAppSubroute] = navigationState.routes; if (firstAppSubroute.name !== CommunityDrawerNavigatorRouteName) { return null; } const communityDrawerState = getStateFromNavigatorRoute(firstAppSubroute); const [firstCommunityDrawerSubroute] = communityDrawerState.routes; if (firstCommunityDrawerSubroute.name !== TabNavigatorRouteName) { return null; } const tabState = getStateFromNavigatorRoute(firstCommunityDrawerSubroute); return tabState; } function getChatNavStateFromTabNavState( tabState: ?PossiblyStaleNavigationState, ): ?PossiblyStaleNavigationState { if (!tabState) { return null; } let chatRoute; for (const route of tabState.routes) { if (route.name === ChatRouteName) { chatRoute = route; break; } } if (!chatRoute || !chatRoute.state) { return null; } const chatRouteState = getStateFromNavigatorRoute(chatRoute); if (chatRouteState.type !== 'stack') { return null; } return chatRouteState; } function getRemoveEditMode( chatRouteState: ?PossiblyStaleNavigationState, ): ?RemoveEditMode { if (!chatRouteState) { return null; } const messageListRoute = chatRouteState.routes[chatRouteState.routes.length - 1]; if (messageListRoute.name !== MessageListRouteName) { return null; } if (!messageListRoute || !messageListRoute.params) { return null; } const removeEditMode: Function = messageListRoute.params.removeEditMode; return removeEditMode; } function useCurrentLeafRouteName(): ?string { const navContext = React.useContext(NavContext); return React.useMemo(() => { if (!navContext) { return undefined; } return currentLeafRoute(navContext.state).name; }, [navContext]); } function useCanEditMessageNative( threadInfo: ThreadInfo, targetMessageInfo: ComposableMessageInfo | RobotextMessageInfo, ): boolean { const route = useRoute(); const screenKey = route.key; const threadCreationTime = threadInfo.creationTime; const messageCreationTime = targetMessageInfo.time; const canEditInThisScreen = !screenKey.startsWith(MessageSearchRouteName) && !screenKey.startsWith(PinnedMessagesScreenRouteName) && messageCreationTime >= threadCreationTime; return ( useCanEditMessage(threadInfo, targetMessageInfo) && canEditInThisScreen ); } export { createIsForegroundSelector, useIsAppLoggedIn, createActiveTabSelector, scrollBlockingModalsClosedSelector, selectBackgroundIsDark, activeThreadSelector, activeMessageListSelector, useActiveThread, useActiveMessageList, calendarActiveSelector, nativeCalendarQuery, nonThreadCalendarQuery, useCalendarQuery, drawerSwipeEnabledSelector, useCurrentLeafRouteName, getRemoveEditMode, getTabNavState, getChatNavStateFromTabNavState, useCanEditMessageNative, + useForegroundActiveThread, }; diff --git a/native/socket.react.js b/native/socket.react.js index 4db9da650..779e7c36e 100644 --- a/native/socket.react.js +++ b/native/socket.react.js @@ -1,131 +1,126 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useFetchPendingUpdates } from 'lib/actions/update-actions.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import { openSocketSelector } from 'lib/selectors/socket-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { - activeMessageListSelector, nativeCalendarQuery, + useForegroundActiveThread, } from './navigation/nav-selectors.js'; import { NavContext } from './navigation/navigation-context.js'; import { useSelector } from './redux/redux-utils.js'; import { sessionIdentificationSelector, nativeGetClientResponsesSelector, nativeSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import { decompressMessage } from './utils/decompress.js'; const NativeSocket: React.ComponentType = React.memo(function NativeSocket(props: BaseSocketProps) { const navContext = React.useContext(NavContext); const { keyserverID } = props; const cookie = useSelector(cookieSelector(keyserverID)); const connection = useSelector(connectionSelector(keyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const frozen = useSelector(state => state.frozen); const active = useSelector( state => isLoggedIn(state) && state.lifecycleState !== 'background', ); const openSocket = useSelector(openSocketSelector(keyserverID)); invariant(openSocket, 'openSocket failed to be created'); const sessionIdentification = useSelector( sessionIdentificationSelector(keyserverID), ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(keyserverID), ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(keyserverID); const getClientResponses = useSelector(state => nativeGetClientResponsesSelector({ redux: state, navContext, getInitialNotificationsEncryptedMessage, keyserverID, }), ); const sessionStateFunc = useSelector(state => nativeSessionStateFuncSelector(keyserverID)({ redux: state, navContext, }), ); const currentCalendarQuery = useSelector(state => nativeCalendarQuery({ redux: state, navContext, }), ); - const activeThread = React.useMemo(() => { - if (!active) { - return null; - } - return activeMessageListSelector(navContext); - }, [active, navContext]); + const activeThread = useForegroundActiveThread(); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(keyserverID), ); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const activeSessionRecovery = useSelector( state => state.keyserverStore.keyserverInfos[keyserverID]?.connection .activeSessionRecovery, ); const fetchPendingUpdates = useFetchPendingUpdates(); const isConnectedToInternet = useSelector( state => state.connectivity.connected, ); return ( ); }); export default NativeSocket; diff --git a/web/components/dm-activity-handler.react.js b/web/components/dm-activity-handler.react.js index 71d55e568..026e4b4f0 100644 --- a/web/components/dm-activity-handler.react.js +++ b/web/components/dm-activity-handler.react.js @@ -1,27 +1,16 @@ // @flow + import * as React from 'react'; import useDMActivityHandler from 'lib/handlers/dm-activity-handler.js'; -import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import { useSelector } from '../redux/redux-utils.js'; -import { activeThreadSelector } from '../selectors/nav-selectors.js'; +import { foregroundActiveThreadSelector } from '../selectors/nav-selectors.js'; function DMActivityHandler(): React.Node { - const active = useSelector( - state => isLoggedIn(state) && state.lifecycleState !== 'background', - ); - const reduxActiveThread = useSelector(activeThreadSelector); - const windowActive = useSelector(state => state.windowActive); - const activeThread = React.useMemo(() => { - if (!active || !windowActive) { - return null; - } - return reduxActiveThread; - }, [active, windowActive, reduxActiveThread]); - + const activeThread = useSelector(foregroundActiveThreadSelector); useDMActivityHandler(activeThread); return null; } export default DMActivityHandler; diff --git a/web/selectors/nav-selectors.js b/web/selectors/nav-selectors.js index 124fd5709..bbb1cac07 100644 --- a/web/selectors/nav-selectors.js +++ b/web/selectors/nav-selectors.js @@ -1,140 +1,156 @@ // @flow import invariant from 'invariant'; import { createSelector } from 'reselect'; import { nonThreadCalendarFiltersSelector } from 'lib/selectors/calendar-filter-selectors.js'; import { currentCalendarQuery } from 'lib/selectors/nav-selectors.js'; +import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { CalendarFilter } from 'lib/types/filter-types.js'; import type { WebNavigationSettingsSection, WebNavigationTab, } from 'lib/types/nav-types.js'; import type { AppState } from '../redux/redux-setup.js'; const dateExtractionRegex = /^([0-9]{4})-([0-9]{2})-[0-9]{2}$/; function yearExtractor(startDate: string, endDate: string): ?number { const startDateResults = dateExtractionRegex.exec(startDate); const endDateResults = dateExtractionRegex.exec(endDate); if ( !startDateResults || !startDateResults[1] || !endDateResults || !endDateResults[1] || startDateResults[1] !== endDateResults[1] ) { return null; } return parseInt(startDateResults[1], 10); } function yearAssertingExtractor(startDate: string, endDate: string): number { const result = yearExtractor(startDate, endDate); invariant( result !== null && result !== undefined, `${startDate} and ${endDate} aren't in the same year`, ); return result; } const yearAssertingSelector: (state: AppState) => number = createSelector( (state: AppState) => state.navInfo.startDate, (state: AppState) => state.navInfo.endDate, yearAssertingExtractor, ); // 1-indexed function monthExtractor(startDate: string, endDate: string): ?number { const startDateResults = dateExtractionRegex.exec(startDate); const endDateResults = dateExtractionRegex.exec(endDate); if ( !startDateResults || !startDateResults[1] || !startDateResults[2] || !endDateResults || !endDateResults[1] || !endDateResults[2] || startDateResults[1] !== endDateResults[1] || startDateResults[2] !== endDateResults[2] ) { return null; } return parseInt(startDateResults[2], 10); } // 1-indexed function monthAssertingExtractor(startDate: string, endDate: string): number { const result = monthExtractor(startDate, endDate); invariant( result !== null && result !== undefined, `${startDate} and ${endDate} aren't in the same month`, ); return result; } // 1-indexed const monthAssertingSelector: (state: AppState) => number = createSelector( (state: AppState) => state.navInfo.startDate, (state: AppState) => state.navInfo.endDate, monthAssertingExtractor, ); function activeThreadSelector(state: AppState): ?string { return state.navInfo.tab === 'chat' ? state.navInfo.activeChatThreadID : null; } +const foregroundActiveThreadSelector: (state: AppState) => ?string = + createSelector( + (state: AppState) => + isLoggedIn(state) && state.lifecycleState !== 'background', + (state: AppState) => state.windowActive, + activeThreadSelector, + (active: boolean, windowActive: boolean, activeThread: ?string) => { + if (!active || !windowActive) { + return null; + } + return activeThread; + }, + ); + const webCalendarQuery: (state: AppState) => () => CalendarQuery = createSelector( currentCalendarQuery, (state: AppState) => state.navInfo.tab === 'calendar', ( calendarQuery: (calendarActive: boolean) => CalendarQuery, calendarActive: boolean, ) => () => calendarQuery(calendarActive), ); const nonThreadCalendarQuery: (state: AppState) => () => CalendarQuery = createSelector( webCalendarQuery, nonThreadCalendarFiltersSelector, ( calendarQuery: () => CalendarQuery, filters: $ReadOnlyArray, ) => { return (): CalendarQuery => { const query = calendarQuery(); return { startDate: query.startDate, endDate: query.endDate, filters, }; }; }, ); function navTabSelector(state: AppState): WebNavigationTab { return state.navInfo.tab; } function navSettingsSectionSelector( state: AppState, ): ?WebNavigationSettingsSection { return state.navInfo.settingsSection; } export { yearExtractor, yearAssertingSelector, monthExtractor, monthAssertingSelector, activeThreadSelector, webCalendarQuery, nonThreadCalendarQuery, navTabSelector, navSettingsSectionSelector, + foregroundActiveThreadSelector, }; diff --git a/web/socket.react.js b/web/socket.react.js index 57f191227..b1c774db1 100644 --- a/web/socket.react.js +++ b/web/socket.react.js @@ -1,119 +1,112 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { useFetchPendingUpdates } from 'lib/actions/update-actions.js'; import { preRequestUserStateForSingleKeyserverSelector } from 'lib/selectors/account-selectors.js'; import { cookieSelector, connectionSelector, lastCommunicatedPlatformDetailsSelector, } from 'lib/selectors/keyserver-selectors.js'; import { openSocketSelector } from 'lib/selectors/socket-selectors.js'; import { useInitialNotificationsEncryptedMessage } from 'lib/shared/crypto-utils.js'; import Socket, { type BaseSocketProps } from 'lib/socket/socket.react.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { useNetworkConnected } from './redux/keyserver-reachability-handler.js'; import { useSelector } from './redux/redux-utils.js'; import { - activeThreadSelector, webCalendarQuery, + foregroundActiveThreadSelector, } from './selectors/nav-selectors.js'; import { sessionIdentificationSelector, webGetClientResponsesSelector, webSessionStateFuncSelector, } from './selectors/socket-selectors.js'; import { decompressMessage } from './utils/decompress.js'; const WebSocket: React.ComponentType = React.memo(function WebSocket(props) { const { keyserverID } = props; const cookie = useSelector(cookieSelector(keyserverID)); const connection = useSelector(connectionSelector(keyserverID)); invariant(connection, 'keyserver missing from keyserverStore'); const active = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous && state.lifecycleState !== 'background', ); const openSocket = useSelector(openSocketSelector(keyserverID)); invariant(openSocket, 'openSocket failed to be created'); const sessionIdentification = useSelector( sessionIdentificationSelector(keyserverID), ); const preRequestUserState = useSelector( preRequestUserStateForSingleKeyserverSelector(keyserverID), ); const getInitialNotificationsEncryptedMessage = useInitialNotificationsEncryptedMessage(keyserverID); const getClientResponses = useSelector(state => webGetClientResponsesSelector({ state, getInitialNotificationsEncryptedMessage, keyserverID, }), ); const sessionStateFunc = useSelector( webSessionStateFuncSelector(keyserverID), ); const currentCalendarQuery = useSelector(webCalendarQuery); - const reduxActiveThread = useSelector(activeThreadSelector); - const windowActive = useSelector(state => state.windowActive); - const activeThread = React.useMemo(() => { - if (!active || !windowActive) { - return null; - } - return reduxActiveThread; - }, [active, windowActive, reduxActiveThread]); + const activeThread = useSelector(foregroundActiveThreadSelector); const dispatch = useDispatch(); const dispatchActionPromise = useDispatchActionPromise(); const lastCommunicatedPlatformDetails = useSelector( lastCommunicatedPlatformDetailsSelector(keyserverID), ); const activeSessionRecovery = useSelector( state => state.keyserverStore.keyserverInfos[keyserverID]?.connection .activeSessionRecovery, ); const fetchPendingUpdates = useFetchPendingUpdates(); const isConnectedToInternet = useNetworkConnected(); return ( ); }); export default WebSocket;