diff --git a/lib/components/modal-overlay.react.js b/lib/components/modal-overlay.react.js index 3c3f349dd..ae3bd4f7a 100644 --- a/lib/components/modal-overlay.react.js +++ b/lib/components/modal-overlay.react.js @@ -1,85 +1,88 @@ // @flow import FocusTrap from 'focus-trap-react'; import * as React from 'react'; import css from './modal-overlay.css'; type ModalOverlayProps = { +onClose: () => void, +children?: React.Node, +backgroundColor?: string, }; const focusTrapOptions = { fallbackFocus: '#modal-overlay', allowOutsideClick: true, }; function ModalOverlay(props: ModalOverlayProps): React.Node { const { children, onClose, backgroundColor = 'var(--modal-overlay-background-90)', } = props; const overlayRef = React.useRef(); - const firstClickRef = React.useRef(null); + const firstClickRef = React.useRef(null); React.useLayoutEffect(() => { if (overlayRef.current) { overlayRef.current.focus(); } }, []); - const onBackgroundMouseDown = React.useCallback(event => { - firstClickRef.current = event.target; - }, []); + const onBackgroundMouseDown = React.useCallback( + (event: SyntheticEvent) => { + firstClickRef.current = event.target; + }, + [], + ); const onBackgroundMouseUp = React.useCallback( - event => { + (event: SyntheticEvent) => { if ( event.target === overlayRef.current && firstClickRef.current === overlayRef.current ) { onClose(); } }, [onClose], ); const onKeyDown = React.useCallback( - event => { + (event: SyntheticKeyboardEvent) => { if (event.key === 'Escape') { onClose(); } }, [onClose], ); const containerStyle = React.useMemo( () => ({ backgroundColor, }), [backgroundColor], ); return ( ); } export default ModalOverlay; diff --git a/lib/components/modal-provider.react.js b/lib/components/modal-provider.react.js index 4c2e230c0..ad5553ec0 100644 --- a/lib/components/modal-provider.react.js +++ b/lib/components/modal-provider.react.js @@ -1,67 +1,67 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { getUUID } from '../utils/uuid.js'; export type PushModal = React.Node => string; type Props = { +children: React.Node, }; type ModalContextType = { +modals: $ReadOnlyArray<[React.Node, string]>, +pushModal: PushModal, +popModal: () => void, +clearModals: () => void, }; const ModalContext: React.Context = React.createContext({ modals: [], pushModal: () => '', popModal: () => {}, clearModals: () => {}, }); function ModalProvider(props: Props): React.Node { const { children } = props; const [modals, setModals] = React.useState< $ReadOnlyArray<[React.Node, string]>, >([]); const popModal = React.useCallback( () => setModals(oldModals => oldModals.slice(0, -1)), [], ); - const pushModal = React.useCallback(newModal => { + const pushModal = React.useCallback((newModal: React.Node) => { const key = getUUID(); setModals(oldModals => [...oldModals, [newModal, key]]); return key; }, []); const clearModals = React.useCallback(() => setModals([]), []); const value = React.useMemo( () => ({ modals, pushModal, popModal, clearModals, }), [modals, pushModal, popModal, clearModals], ); return ( {children} ); } function useModalContext(): ModalContextType { const context = React.useContext(ModalContext); invariant(context, 'ModalContext not found'); return context; } export { ModalProvider, useModalContext }; diff --git a/lib/hooks/child-threads.js b/lib/hooks/child-threads.js index f2e6204d8..fb79be0ce 100644 --- a/lib/hooks/child-threads.js +++ b/lib/hooks/child-threads.js @@ -1,117 +1,133 @@ // @flow import * as React from 'react'; import { useFetchSingleMostRecentMessagesFromThreads, fetchSingleMostRecentMessagesFromThreadsActionTypes, } from '../actions/message-actions.js'; import { useFilteredChatListData, type ChatThreadItem, } from '../selectors/chat-selectors.js'; import { useGlobalThreadSearchIndex } from '../selectors/nav-selectors.js'; import { childThreadInfos } from '../selectors/thread-selectors.js'; import { threadInChatList } from '../shared/thread-utils.js'; import threadWatcher from '../shared/thread-watcher.js'; -import type { MinimallyEncodedThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; -import type { ThreadInfo } from '../types/thread-types.js'; +import type { + MinimallyEncodedThreadInfo, + MinimallyEncodedRawThreadInfo, +} from '../types/minimally-encoded-thread-permissions-types.js'; +import type { ThreadInfo, RawThreadInfo } from '../types/thread-types.js'; import { useDispatchActionPromise } from '../utils/action-utils.js'; import { useSelector } from '../utils/redux-utils.js'; type ThreadFilter = { +predicate?: (thread: ThreadInfo | MinimallyEncodedThreadInfo) => boolean, +searchText?: string, }; function useFilteredChildThreads( threadID: string, filter?: ThreadFilter, ): $ReadOnlyArray { const defaultPredicate = React.useCallback(() => true, []); const { predicate = defaultPredicate, searchText = '' } = filter ?? {}; const childThreads = useSelector(state => childThreadInfos(state)[threadID]); const subchannelIDs = React.useMemo(() => { if (!childThreads) { return new Set(); } return new Set( childThreads.filter(predicate).map(threadInfo => threadInfo.id), ); }, [childThreads, predicate]); const filterSubchannels = React.useCallback( - thread => subchannelIDs.has(thread?.id), + ( + thread: ?( + | ThreadInfo + | RawThreadInfo + | MinimallyEncodedThreadInfo + | MinimallyEncodedRawThreadInfo + ), + ) => { + const candidateThreadID = thread?.id; + if (!candidateThreadID) { + return false; + } + return subchannelIDs.has(candidateThreadID); + }, [subchannelIDs], ); const allSubchannelsList = useFilteredChatListData(filterSubchannels); const searchIndex = useGlobalThreadSearchIndex(); const searchResultIDs = React.useMemo( () => searchIndex.getSearchResults(searchText), [searchIndex, searchText], ); const searchTextExists = !!searchText.length; const subchannelIDsNotInChatList = React.useMemo( () => new Set( allSubchannelsList .filter(item => !threadInChatList(item.threadInfo)) .map(item => item.threadInfo.id), ), [allSubchannelsList], ); React.useEffect(() => { if (!subchannelIDsNotInChatList.size) { return undefined; } subchannelIDsNotInChatList.forEach(tID => threadWatcher.watchID(tID)); return () => subchannelIDsNotInChatList.forEach(tID => threadWatcher.removeID(tID)); }, [subchannelIDsNotInChatList]); const filteredSubchannelsChatList = React.useMemo(() => { if (!searchTextExists) { return allSubchannelsList; } return allSubchannelsList.filter(item => searchResultIDs.includes(item.threadInfo.id), ); }, [allSubchannelsList, searchResultIDs, searchTextExists]); const threadIDsWithNoMessages = React.useMemo( () => new Set( filteredSubchannelsChatList .filter(item => !item.mostRecentMessageInfo) .map(item => item.threadInfo.id), ), [filteredSubchannelsChatList], ); const dispatchActionPromise = useDispatchActionPromise(); const fetchSingleMostRecentMessages = useFetchSingleMostRecentMessagesFromThreads(); React.useEffect(() => { if (!threadIDsWithNoMessages.size) { return; } dispatchActionPromise( fetchSingleMostRecentMessagesFromThreadsActionTypes, fetchSingleMostRecentMessages(Array.from(threadIDsWithNoMessages)), ); }, [ threadIDsWithNoMessages, fetchSingleMostRecentMessages, dispatchActionPromise, ]); return filteredSubchannelsChatList; } export { useFilteredChildThreads }; diff --git a/lib/shared/state-sync/entries-state-sync-spec.js b/lib/shared/state-sync/entries-state-sync-spec.js index 60f6762c9..7fa9a1d3b 100644 --- a/lib/shared/state-sync/entries-state-sync-spec.js +++ b/lib/shared/state-sync/entries-state-sync-spec.js @@ -1,107 +1,109 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec } from './state-sync-spec.js'; import { type CalendarQuery, type RawEntryInfos, type RawEntryInfo, } from '../../types/entry-types.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientEntryInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { values, combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { ashoatKeyserverID } from '../../utils/validation-utils.js'; import { filterRawEntryInfosByCalendarQuery, serverEntryInfosObject, } from '../entry-utils.js'; export const entriesStateSyncSpec: StateSyncSpec< RawEntryInfos, RawEntryInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'entryInfos', innerHashSpec: { hashKey: 'entryInfo', deleteKey: 'deleteEntryIDs', rawInfosKey: 'rawEntryInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawEntryInfos, afterStateCheck: RawEntryInfos, ) { const calendarQuery = action.payload.calendarQuery; // We don't want to bother reporting an inconsistency if it's just because // of extraneous EntryInfos (not within the current calendarQuery) on either // side const filteredBeforeResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(beforeStateCheck)), calendarQuery, ); const filteredAfterResult = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(afterStateCheck)), calendarQuery, ); if (_isEqual(filteredBeforeResult)(filteredAfterResult)) { return emptyArray; } return [ { type: reportTypes.ENTRY_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), calendarQuery, pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector: createSelector( (state: AppState) => state.entryStore.entryInfos, entryInfos => ({ ...entriesStateSyncSpec, - getInfoHash: id => hash(entryInfos[`${ashoatKeyserverID}|${id}`]), - getAllInfosHash: calendarQuery => + getInfoHash: (id: string) => + hash(entryInfos[`${ashoatKeyserverID}|${id}`]), + getAllInfosHash: (calendarQuery: CalendarQuery) => getEntryInfosHash(entryInfos, calendarQuery), - getIDs: calendarQuery => getEntryIDs(entryInfos, calendarQuery), + getIDs: (calendarQuery: CalendarQuery) => + getEntryIDs(entryInfos, calendarQuery), }), ), }); const emptyArray = []; function getEntryInfosHash( entryInfos: RawEntryInfos, calendarQuery: CalendarQuery, ) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery, ); return combineUnorderedHashes(Object.values(filteredEntryInfos).map(hash)); } function getEntryIDs(entryInfos: RawEntryInfos, calendarQuery: CalendarQuery) { const filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery, ); return Object.keys(filteredEntryInfos).map(id => id.split('|')[1]); } diff --git a/lib/shared/state-sync/threads-state-sync-spec.js b/lib/shared/state-sync/threads-state-sync-spec.js index 0f54d58eb..a1bdfef87 100644 --- a/lib/shared/state-sync/threads-state-sync-spec.js +++ b/lib/shared/state-sync/threads-state-sync-spec.js @@ -1,73 +1,73 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec } from './state-sync-spec.js'; import type { AppState } from '../../types/redux-types.js'; import { reportTypes, type ClientThreadInconsistencyReportCreationRequest, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type RawThreadInfos, type RawThreadInfo, } from '../../types/thread-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { combineUnorderedHashes, values } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; import { ashoatKeyserverID } from '../../utils/validation-utils.js'; export const threadsStateSyncSpec: StateSyncSpec< RawThreadInfos, RawThreadInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'threadInfos', innerHashSpec: { hashKey: 'threadInfo', deleteKey: 'deleteThreadIDs', rawInfosKey: 'rawThreadInfos', }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: RawThreadInfos, afterStateCheck: RawThreadInfos, ) { if (_isEqual(beforeStateCheck)(afterStateCheck)) { return emptyArray; } return [ { type: reportTypes.THREAD_INCONSISTENCY, platformDetails: getConfig().platformDetails, beforeAction: beforeStateCheck, action: sanitizeActionSecrets(action), pushResult: afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector: createSelector( (state: AppState) => state.integrityStore.threadHashes, (state: AppState) => state.integrityStore.threadHashingStatus === 'completed', (threadHashes, threadHashingComplete) => ({ ...threadsStateSyncSpec, - getInfoHash: id => threadHashes[`${ashoatKeyserverID}|${id}`], + getInfoHash: (id: string) => threadHashes[`${ashoatKeyserverID}|${id}`], getAllInfosHash: threadHashingComplete ? () => combineUnorderedHashes(values(threadHashes)) : () => null, getIDs: threadHashingComplete ? () => Object.keys(threadHashes).map(id => id.split('|')[1]) : () => null, }), ), }); const emptyArray = []; diff --git a/lib/shared/state-sync/users-state-sync-spec.js b/lib/shared/state-sync/users-state-sync-spec.js index 084998c98..1be312962 100644 --- a/lib/shared/state-sync/users-state-sync-spec.js +++ b/lib/shared/state-sync/users-state-sync-spec.js @@ -1,67 +1,67 @@ // @flow import _isEqual from 'lodash/fp/isEqual.js'; import { createSelector } from 'reselect'; import type { StateSyncSpec } from './state-sync-spec.js'; import type { AppState } from '../../types/redux-types'; import { type ClientUserInconsistencyReportCreationRequest, reportTypes, } from '../../types/report-types.js'; import type { ProcessServerRequestAction } from '../../types/request-types.js'; import { type UserInfo, type UserInfos } from '../../types/user-types.js'; import { actionLogger } from '../../utils/action-logger.js'; import { getConfig } from '../../utils/config.js'; import { combineUnorderedHashes, hash } from '../../utils/objects.js'; import { generateReportID } from '../../utils/report-utils.js'; import { sanitizeActionSecrets } from '../../utils/sanitization.js'; export const usersStateSyncSpec: StateSyncSpec< UserInfos, UserInfo, $ReadOnlyArray, > = Object.freeze({ hashKey: 'userInfos', innerHashSpec: { hashKey: 'userInfo', deleteKey: 'deleteUserInfoIDs', rawInfosKey: 'userInfos', additionalDeleteCondition(user: UserInfo) { return !user.username; }, }, findStoreInconsistencies( action: ProcessServerRequestAction, beforeStateCheck: UserInfos, afterStateCheck: UserInfos, ) { if (_isEqual(beforeStateCheck)(afterStateCheck)) { return emptyArray; } return [ { type: reportTypes.USER_INCONSISTENCY, platformDetails: getConfig().platformDetails, action: sanitizeActionSecrets(action), beforeStateCheck, afterStateCheck, lastActions: actionLogger.interestingActionSummaries, time: Date.now(), id: generateReportID(), }, ]; }, selector: createSelector( (state: AppState) => state.userStore.userInfos, userInfos => ({ ...usersStateSyncSpec, - getInfoHash: id => hash(userInfos[id]), + getInfoHash: (id: string) => hash(userInfos[id]), getAllInfosHash: () => combineUnorderedHashes(Object.values(userInfos).map(hash)), getIDs: () => Object.keys(userInfos), }), ), }); const emptyArray = [];