Page MenuHomePhabricator

No OneTemporary

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<FilterThreadInfo>,
+filterThreadSearchIndex: SearchIndex,
+filteredThreadIDs: ?$ReadOnlySet<string>,
+ +filteredCommunityThreadIDs: ?$ReadOnlySet<string>,
+includeDeleted: boolean,
+dispatch: Dispatch,
+pushModal: PushModal,
};
type State = {
+query: string,
+searchResults: $ReadOnlyArray<FilterThreadInfo>,
+collapsed: boolean,
};
class FilterPanel extends React.PureComponent<Props, State> {
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(
<Category
- numThreads={filterThreadInfos.length}
+ numThreads={filterThreadInfosInCurrentCommunity.length}
onToggle={this.onToggleAll}
collapsed={this.state.collapsed}
onCollapse={this.onCollapse}
- selected={!this.props.filteredThreadIDs}
+ selected={
+ !this.props.filteredThreadIDs ||
+ this.props.filteredThreadIDs.size ===
+ this.props.filteredCommunityThreadIDs?.size
+ }
key="all"
/>,
);
} else {
filters.push(
<div className={css.noResults} key="noResults">
No results
</div>,
);
}
if (!this.state.collapsed) {
- const options = filterThreadInfos.map(filterThreadInfo => (
- <Item
- filterThreadInfo={filterThreadInfo}
- onToggle={this.onToggle}
- onClickOnly={this.onClickOnly}
- onClickSettings={this.onClickSettings}
- selected={this.currentlySelected(filterThreadInfo.threadInfo.id)}
- key={filterThreadInfo.threadInfo.id}
- />
- ));
+ const options = filterThreadInfosInCurrentCommunity.map(
+ filterThreadInfo => (
+ <Item
+ filterThreadInfo={filterThreadInfo}
+ onToggle={this.onToggle}
+ onClickOnly={this.onClickOnly}
+ onClickSettings={this.onClickSettings}
+ selected={this.currentlySelected(filterThreadInfo.threadInfo.id)}
+ key={filterThreadInfo.threadInfo.id}
+ />
+ ),
+ );
filters = [...filters, ...options];
}
let clearQueryButton = null;
if (this.state.query) {
clearQueryButton = (
<a href="#" onClick={this.clearQuery}>
<FontAwesomeIcon icon={faTimesCircle} className={css.clearQuery} />
</a>
);
}
return (
<div className={css.container}>
<div className={css.searchContainer}>
<div className={css.search}>
<MagnifyingGlass className={css.searchVector} />
<input
type="text"
placeholder="Search"
value={this.state.query}
onChange={this.onChangeQuery}
/>
{clearQueryButton}
</div>
</div>
<div className={css.filters}>{filters}</div>
<div className={css.extras}>
<label htmlFor="include-deleted-switch">
<Switch
checked={this.props.includeDeleted}
onChange={this.onChangeIncludeDeleted}
checkedIcon={false}
uncheckedIcon={false}
height={20}
width={40}
id="include-deleted-switch"
/>
<span>Include deleted entries</span>
</label>
</div>
</div>
);
}
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<string>) {
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(<ThreadSettingsModal threadID={threadID} />);
};
onChangeQuery = (event: SyntheticEvent<HTMLInputElement>) => {
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<HTMLAnchorElement>) => {
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<ItemProps> {
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 = (
<div
className={classNames(css.optionCheckbox, css.checkboxAfterOption)}
style={afterCheckStyles}
/>
);
}
const details =
this.props.filterThreadInfo.numVisibleEntries === 1
? '1 entry'
: `${this.props.filterThreadInfo.numVisibleEntries} entries`;
return (
<div className={css.option}>
<div className={css.optionThread}>
<input
type="checkbox"
checked={this.props.selected}
onChange={this.onChange}
/>
<label>
<div className={css.optionCheckbox} style={beforeCheckStyles} />
{threadInfo.uiName}
{afterCheck}
</label>
<a onClick={this.onClickOnly} className={css.only}>
only
</a>
<a onClick={this.onClickSettings} className={css.settingsCog}>
<FontAwesomeIcon icon={faCog} />
</a>
</div>
<div className={css.optionDetails}>{details}</div>
</div>
);
}
onChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.props.onToggle(
this.props.filterThreadInfo.threadInfo.id,
event.currentTarget.checked,
);
};
onClickOnly = (event: SyntheticEvent<HTMLAnchorElement>) => {
event.preventDefault();
this.props.onClickOnly(this.props.filterThreadInfo.threadInfo.id);
};
onClickSettings = (event: SyntheticEvent<HTMLAnchorElement>) => {
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<CategoryProps> {
render() {
const beforeCheckStyles = { borderColor: 'white' };
let afterCheck = null;
if (this.props.selected) {
const afterCheckStyles = { backgroundColor: 'white' };
afterCheck = (
<div
className={classNames(css.optionCheckbox, css.checkboxAfterOption)}
style={afterCheckStyles}
/>
);
}
const icon = this.props.collapsed ? faChevronUp : faChevronDown;
const details =
this.props.numThreads === 1 ? '1 chat' : `${this.props.numThreads} chats`;
return (
<div className={css.category}>
<div className={css.optionThread}>
<input
type="checkbox"
checked={this.props.selected}
onChange={this.onChange}
/>
<label>
<div className={css.optionCheckbox} style={beforeCheckStyles} />
Your chats
{afterCheck}
</label>
<a onClick={this.onCollapse} className={css.collapse}>
<FontAwesomeIcon icon={icon} />
</a>
</div>
<div className={css.optionDetails}>{details}</div>
</div>
);
}
onChange = (event: SyntheticEvent<HTMLInputElement>) => {
this.props.onToggle(event.currentTarget.checked);
};
onCollapse = (event: SyntheticEvent<HTMLAnchorElement>) => {
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 (
<FilterPanel
filteredThreadIDs={filteredThreadIDs}
+ filteredCommunityThreadIDs={filteredCommunityThreadIDs}
filterThreadInfos={filterThreadInfos}
filterThreadSearchIndex={filterThreadSearchIndex}
includeDeleted={includeDeleted}
dispatch={dispatch}
pushModal={modalContext.pushModal}
/>
);
},
);
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<HandlerProps>,
} = Object.freeze({
chat: ChatDrawerItemHandler,
calendar: CalendarDrawerItemHandler,
});
function getCommunityDrawerItemHandler(
tab: NavigationTab,
): React.ComponentType<HandlerProps> {
return communityDrawerItemHandlers[tab] ?? ChatDrawerItemHandler;
}
export { getCommunityDrawerItemHandler };

File Metadata

Mime Type
text/x-diff
Expires
Mon, Dec 23, 10:47 AM (13 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2686537
Default Alt Text
(16 KB)

Event Timeline