diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js index 84b857c9f..f98d28142 100644 --- a/web/calendar/filter-panel.react.js +++ b/web/calendar/filter-panel.react.js @@ -1,384 +1,413 @@ // @flow import { faCog, faTimesCircle, faChevronUp, faChevronDown, } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import classNames from 'classnames'; import * as React from 'react'; import { useDispatch } from 'react-redux'; import Switch from 'react-switch'; import { useModalContext, type PushModal, } from 'lib/components/modal-provider.react.js'; import { filteredThreadIDsSelector, includeDeletedSelector, } from 'lib/selectors/calendar-filter-selectors.js'; import SearchIndex from 'lib/shared/search-index.js'; import { calendarThreadFilterTypes, type FilterThreadInfo, updateCalendarThreadFilter, clearCalendarThreadFilter, setCalendarDeletedFilter, } from 'lib/types/filter-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import css from './filter-panel.css'; import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { useFilterThreadInfos, useFilterThreadSearchIndex, + filterThreadIDsBelongingToCommunitySelector, } from '../selectors/calendar-selectors.js'; import { MagnifyingGlass } from '../vectors.react.js'; type Props = { +filterThreadInfos: $ReadOnlyArray, +filterThreadSearchIndex: SearchIndex, +filteredThreadIDs: ?$ReadOnlySet, + +filteredCommunityThreadIDs: ?$ReadOnlySet, +includeDeleted: boolean, +dispatch: Dispatch, +pushModal: PushModal, }; type State = { +query: string, +searchResults: $ReadOnlyArray, +collapsed: boolean, }; class FilterPanel extends React.PureComponent { state: State = { query: '', searchResults: [], collapsed: false, }; currentlySelected(threadID: string): boolean { if (!this.props.filteredThreadIDs) { return true; } return this.props.filteredThreadIDs.has(threadID); } + inCurrentCommunity(threadID: string): boolean { + if (!this.props.filteredCommunityThreadIDs) { + return true; + } + return this.props.filteredCommunityThreadIDs.has(threadID); + } + render() { const filterThreadInfos = this.state.query ? this.state.searchResults : this.props.filterThreadInfos; + const filterThreadInfosInCurrentCommunity = filterThreadInfos.filter(item => + this.inCurrentCommunity(item.threadInfo.id), + ); let filters = []; - if (!this.state.query || filterThreadInfos.length > 0) { + if (!this.state.query || filterThreadInfosInCurrentCommunity.length > 0) { filters.push( , ); } else { filters.push(
No results
, ); } if (!this.state.collapsed) { - const options = filterThreadInfos.map(filterThreadInfo => ( - - )); + const options = filterThreadInfosInCurrentCommunity.map( + filterThreadInfo => ( + + ), + ); filters = [...filters, ...options]; } let clearQueryButton = null; if (this.state.query) { clearQueryButton = ( ); } return (
{clearQueryButton}
{filters}
); } onToggle = (threadID: string, value: boolean) => { let newThreadIDs; const selectedThreadIDs = this.props.filteredThreadIDs; if (!selectedThreadIDs && value) { // No thread filter exists and thread is being added return; } else if (!selectedThreadIDs) { // No thread filter exists and thread is being removed newThreadIDs = this.props.filterThreadInfos .map(filterThreadInfo => filterThreadInfo.threadInfo.id) .filter(id => id !== threadID); } else if (selectedThreadIDs.has(threadID) && value) { // Thread filter already includes thread being added return; } else if (selectedThreadIDs.has(threadID)) { // Thread being removed from current thread filter newThreadIDs = [...selectedThreadIDs].filter(id => id !== threadID); } else if (!value) { // Thread filter doesn't include thread being removed return; } else if ( selectedThreadIDs.size + 1 === this.props.filterThreadInfos.length ) { // Thread filter exists and thread being added is the only one missing newThreadIDs = null; } else { // Thread filter exists and thread is being added newThreadIDs = [...selectedThreadIDs, threadID]; } this.setFilterThreads(newThreadIDs); }; onToggleAll = (value: boolean) => { - this.setFilterThreads(value ? null : []); + if (!value) { + this.setFilterThreads([]); + return; + } + const allChats = this.props.filteredCommunityThreadIDs + ? Array.from(this.props.filteredCommunityThreadIDs) + : null; + this.setFilterThreads(allChats); }; onClickOnly = (threadID: string) => { this.setFilterThreads([threadID]); }; setFilterThreads(threadIDs: ?$ReadOnlyArray) { if (!threadIDs) { this.props.dispatch({ type: clearCalendarThreadFilter, }); } else { this.props.dispatch({ type: updateCalendarThreadFilter, payload: { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs, }, }); } } onClickSettings = (threadID: string) => { this.props.pushModal(); }; onChangeQuery = (event: SyntheticEvent) => { const query = event.currentTarget.value; const searchIndex = this.props.filterThreadSearchIndex; const resultIDs = new Set(searchIndex.getSearchResults(query)); const results = this.props.filterThreadInfos.filter(filterThreadInfo => resultIDs.has(filterThreadInfo.threadInfo.id), ); this.setState({ query, searchResults: results, collapsed: false }); }; clearQuery = (event: SyntheticEvent) => { event.preventDefault(); this.setState({ query: '', searchResults: [], collapsed: false }); }; onCollapse = (value: boolean) => { this.setState({ collapsed: value }); }; onChangeIncludeDeleted = (includeDeleted: boolean) => { this.props.dispatch({ type: setCalendarDeletedFilter, payload: { includeDeleted, }, }); }; } type ItemProps = { +filterThreadInfo: FilterThreadInfo, +onToggle: (threadID: string, value: boolean) => void, +onClickOnly: (threadID: string) => void, +onClickSettings: (threadID: string) => void, +selected: boolean, }; class Item extends React.PureComponent { render() { const threadInfo = this.props.filterThreadInfo.threadInfo; const beforeCheckStyles = { borderColor: `#${threadInfo.color}` }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: `#${threadInfo.color}` }; afterCheck = (
); } const details = this.props.filterThreadInfo.numVisibleEntries === 1 ? '1 entry' : `${this.props.filterThreadInfo.numVisibleEntries} entries`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle( this.props.filterThreadInfo.threadInfo.id, event.currentTarget.checked, ); }; onClickOnly = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickOnly(this.props.filterThreadInfo.threadInfo.id); }; onClickSettings = (event: SyntheticEvent) => { event.preventDefault(); this.props.onClickSettings(this.props.filterThreadInfo.threadInfo.id); }; } type CategoryProps = { +numThreads: number, +onToggle: (value: boolean) => void, +collapsed: boolean, +onCollapse: (value: boolean) => void, +selected: boolean, }; class Category extends React.PureComponent { render() { const beforeCheckStyles = { borderColor: 'white' }; let afterCheck = null; if (this.props.selected) { const afterCheckStyles = { backgroundColor: 'white' }; afterCheck = (
); } const icon = this.props.collapsed ? faChevronUp : faChevronDown; const details = this.props.numThreads === 1 ? '1 chat' : `${this.props.numThreads} chats`; return (
); } onChange = (event: SyntheticEvent) => { this.props.onToggle(event.currentTarget.checked); }; onCollapse = (event: SyntheticEvent) => { event.preventDefault(); this.props.onCollapse(!this.props.collapsed); }; } const ConnectedFilterPanel: React.ComponentType<{}> = React.memo<{}>( function ConnectedFilterPanel(): React.Node { const filteredThreadIDs = useSelector(filteredThreadIDsSelector); + const filteredCommunityThreadIDs = useSelector( + filterThreadIDsBelongingToCommunitySelector, + ); const filterThreadInfos = useFilterThreadInfos(); const filterThreadSearchIndex = useFilterThreadSearchIndex(); const includeDeleted = useSelector(includeDeletedSelector); const dispatch = useDispatch(); const modalContext = useModalContext(); return ( ); }, ); export default ConnectedFilterPanel; diff --git a/web/sidebar/community-drawer-item-handlers.react.js b/web/sidebar/community-drawer-item-handlers.react.js index 4fd5bb9c6..8260daed5 100644 --- a/web/sidebar/community-drawer-item-handlers.react.js +++ b/web/sidebar/community-drawer-item-handlers.react.js @@ -1,81 +1,81 @@ // @flow import * as React from 'react'; import { useDispatch } from 'react-redux'; import { updateCalendarThreadFilter, calendarThreadFilterTypes, } from 'lib/types/filter-types.js'; -import type { ThreadInfo } from 'lib/types/thread-types'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; import type { CommunityDrawerItemHandler } from './community-drawer-item-handler.react.js'; import { useOnClickThread, useThreadIsActive, } from '../selectors/thread-selectors.js'; -import type { NavigationTab } from '../types/nav-types'; +import type { NavigationTab } from '../types/nav-types.js'; type HandlerProps = { +setHandler: (handler: CommunityDrawerItemHandler) => void, +threadInfo: ThreadInfo, }; function ChatDrawerItemHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const onClick = useOnClickThread(threadInfo); const isActive = useThreadIsActive(threadInfo.id); const handler = React.useMemo(() => ({ onClick, isActive }), [ isActive, onClick, ]); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } function CalendarDrawerItemHandler(props: HandlerProps): React.Node { const { setHandler, threadInfo } = props; const dispatch = useDispatch(); const onClick = React.useCallback( () => dispatch({ type: updateCalendarThreadFilter, payload: { type: calendarThreadFilterTypes.THREAD_LIST, threadIDs: [threadInfo.id], }, }), [dispatch, threadInfo.id], ); const isActive = false; const handler = React.useMemo(() => ({ onClick, isActive }), [ onClick, isActive, ]); React.useEffect(() => { setHandler(handler); }, [handler, setHandler]); return null; } const communityDrawerItemHandlers: { +[tab: NavigationTab]: React.ComponentType, } = Object.freeze({ chat: ChatDrawerItemHandler, calendar: CalendarDrawerItemHandler, }); function getCommunityDrawerItemHandler( tab: NavigationTab, ): React.ComponentType { return communityDrawerItemHandlers[tab] ?? ChatDrawerItemHandler; } export { getCommunityDrawerItemHandler };