diff --git a/web/calendar/filter-panel.react.js b/web/calendar/filter-panel.react.js index 50fbace97..006b28a2c 100644 --- a/web/calendar/filter-panel.react.js +++ b/web/calendar/filter-panel.react.js @@ -1,386 +1,386 @@ // @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 { filteredThreadIDsSelector, includeDeletedSelector, } from 'lib/selectors/calendar-filter-selectors'; import SearchIndex from 'lib/shared/search-index'; import { calendarThreadFilterTypes, type FilterThreadInfo, updateCalendarThreadFilter, clearCalendarThreadFilter, setCalendarDeletedFilter, } from 'lib/types/filter-types'; import type { Dispatch } from 'lib/types/redux-types'; import { useModalContext } from '../modals/modal-provider.react'; -import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; +import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import { webFilterThreadInfos, webFilterThreadSearchIndex, } from '../selectors/calendar-selectors'; import { MagnifyingGlass } from '../vectors.react'; import css from './filter-panel.css'; type Props = { +filterThreadInfos: () => $ReadOnlyArray, +filterThreadSearchIndex: () => SearchIndex, +filteredThreadIDs: ?$ReadOnlySet, +includeDeleted: boolean, +dispatch: Dispatch, +pushModal: (modal: React.Node) => void, }; 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); } render() { const filterThreadInfos = this.state.query ? this.state.searchResults : this.props.filterThreadInfos(); let filters = []; if (!this.state.query || filterThreadInfos.length > 0) { filters.push( , ); } else { filters.push(
No results
, ); } if (!this.state.collapsed) { const options = filterThreadInfos.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 : []); }; 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 thread' : `${this.props.numThreads} threads`; 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 filterThreadInfos = useSelector(webFilterThreadInfos); const filterThreadSearchIndex = useSelector(webFilterThreadSearchIndex); const includeDeleted = useSelector(includeDeletedSelector); const dispatch = useDispatch(); const modalContext = useModalContext(); return ( ); }, ); export default ConnectedFilterPanel; diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js index 2fe8ccefd..ead4fa6c5 100644 --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -1,280 +1,280 @@ // @flow import * as React from 'react'; import { leaveThread, leaveThreadActionTypes, } from 'lib/actions/thread-actions'; import { usePromoteSidebar } from 'lib/hooks/promote-sidebar.react'; import { childThreadInfos } from 'lib/selectors/thread-selectors'; import { threadHasPermission, viewerIsMember, threadIsChannel, } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadTypes, threadPermissions, } from 'lib/types/thread-types'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import MenuItem from '../components/menu-item.react'; import Menu from '../components/menu.react'; import SidebarListModal from '../modals/chat/sidebar-list-modal.react'; import SidebarPromoteModal from '../modals/chat/sidebar-promote-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react'; import ThreadMembersModal from '../modals/threads/members/members-modal.react'; import ThreadNotificationsModal from '../modals/threads/notifications/notifications-modal.react'; +import ThreadSettingsModal from '../modals/threads/settings/thread-settings-modal.react'; import SubchannelsModal from '../modals/threads/subchannels/subchannels-modal.react'; -import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; import css from './thread-menu.css'; type ThreadMenuProps = { +threadInfo: ThreadInfo, }; function ThreadMenu(props: ThreadMenuProps): React.Node { const { pushModal, popModal } = useModalContext(); const { threadInfo } = props; const { onPromoteSidebar, canPromoteSidebar } = usePromoteSidebar(threadInfo); const onClickSettings = React.useCallback( () => pushModal(), [pushModal, threadInfo.id], ); const settingsItem = React.useMemo(() => { return ( ); }, [onClickSettings]); const onClickMembers = React.useCallback( () => pushModal( , ), [popModal, pushModal, threadInfo.id], ); const membersItem = React.useMemo(() => { if (threadInfo.type === threadTypes.PERSONAL) { return null; } return ( ); }, [onClickMembers, threadInfo.type]); const childThreads = useSelector( state => childThreadInfos(state)[threadInfo.id], ); const hasSidebars = React.useMemo(() => { return childThreads?.some( childThreadInfo => childThreadInfo.type === threadTypes.SIDEBAR, ); }, [childThreads]); const onClickSidebars = React.useCallback( () => pushModal(), [pushModal, threadInfo], ); const sidebarItem = React.useMemo(() => { if (!hasSidebars) { return null; } return ( ); }, [hasSidebars, onClickSidebars]); const canCreateSubchannels = React.useMemo( () => threadHasPermission(threadInfo, threadPermissions.CREATE_SUBCHANNELS), [threadInfo], ); const hasSubchannels = React.useMemo(() => { return !!childThreads?.some(threadIsChannel); }, [childThreads]); const onClickViewSubchannels = React.useCallback( () => pushModal( , ), [popModal, pushModal, threadInfo.id], ); const viewSubchannelsItem = React.useMemo(() => { if (!hasSubchannels) { return null; } return ( ); }, [hasSubchannels, onClickViewSubchannels]); const createSubchannelsItem = React.useMemo(() => { if (!canCreateSubchannels) { return null; } return ( ); }, [canCreateSubchannels]); const dispatchActionPromise = useDispatchActionPromise(); const callLeaveThread = useServerCall(leaveThread); const onConfirmLeaveThread = React.useCallback(() => { dispatchActionPromise( leaveThreadActionTypes, callLeaveThread(threadInfo.id), ); popModal(); }, [callLeaveThread, popModal, dispatchActionPromise, threadInfo.id]); const onClickLeaveThread = React.useCallback( () => pushModal( , ), [popModal, onConfirmLeaveThread, pushModal, threadInfo], ); const leaveThreadItem = React.useMemo(() => { const canLeaveThread = threadHasPermission( threadInfo, threadPermissions.LEAVE_THREAD, ); if (!viewerIsMember(threadInfo) || !canLeaveThread) { return null; } return ( ); }, [onClickLeaveThread, threadInfo]); const onClickPromoteSidebarToThread = React.useCallback( () => pushModal( , ), [pushModal, threadInfo, popModal, onPromoteSidebar], ); const promoteSidebar = React.useMemo(() => { return ( ); }, [onClickPromoteSidebarToThread]); const onClickNotifications = React.useCallback(() => { pushModal( , ); }, [popModal, pushModal, threadInfo.id]); const notificationsItem = React.useMemo(() => { if (!viewerIsMember(threadInfo)) { return null; } return ( ); }, [onClickNotifications, threadInfo]); const menuItems = React.useMemo(() => { const separator =
; // TODO: Enable menu items when the modals are implemented const SHOW_CREATE_SUBCHANNELS = false; const items = [ settingsItem, notificationsItem, membersItem, sidebarItem, viewSubchannelsItem, SHOW_CREATE_SUBCHANNELS && createSubchannelsItem, leaveThreadItem && separator, canPromoteSidebar && promoteSidebar, leaveThreadItem, ]; return items.filter(Boolean); }, [ settingsItem, notificationsItem, membersItem, sidebarItem, viewSubchannelsItem, promoteSidebar, createSubchannelsItem, leaveThreadItem, canPromoteSidebar, ]); const icon = React.useMemo( () => , [], ); return {menuItems}; } export default ThreadMenu; diff --git a/web/modals/threads/thread-settings-delete-tab.css b/web/modals/threads/settings/thread-settings-delete-tab.css similarity index 100% rename from web/modals/threads/thread-settings-delete-tab.css rename to web/modals/threads/settings/thread-settings-delete-tab.css diff --git a/web/modals/threads/thread-settings-delete-tab.react.js b/web/modals/threads/settings/thread-settings-delete-tab.react.js similarity index 91% rename from web/modals/threads/thread-settings-delete-tab.react.js rename to web/modals/threads/settings/thread-settings-delete-tab.react.js index 9e4c11bef..866c2dcd1 100644 --- a/web/modals/threads/thread-settings-delete-tab.react.js +++ b/web/modals/threads/settings/thread-settings-delete-tab.react.js @@ -1,117 +1,117 @@ // @flow import * as React from 'react'; import { deleteThreadActionTypes, deleteThread, } from 'lib/actions/thread-actions'; -import { type SetState } from 'lib/types/hook-types.js'; +import { type SetState } from 'lib/types/hook-types'; import { type ThreadInfo } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; -import Button from '../../components/button.react.js'; -import SWMansionIcon from '../../SWMansionIcon.react'; -import Input from '../input.react.js'; -import { useModalContext } from '../modal-provider.react.js'; +import Button from '../../../components/button.react'; +import SWMansionIcon from '../../../SWMansionIcon.react'; +import Input from '../../input.react'; +import { useModalContext } from '../../modal-provider.react'; import css from './thread-settings-delete-tab.css'; type ThreadSettingsDeleteTabProps = { +inputDisabled: boolean, +threadInfo: ThreadInfo, +setErrorMessage: SetState, }; function ThreadSettingsDeleteTab( props: ThreadSettingsDeleteTabProps, ): React.Node { const { inputDisabled, threadInfo, setErrorMessage } = props; const modalContext = useModalContext(); const dispatchActionPromise = useDispatchActionPromise(); const callDeleteThread = useServerCall(deleteThread); const accountPasswordInputRef = React.useRef(); const [accountPassword, setAccountPassword] = React.useState(''); const onChangeAccountPassword = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setAccountPassword(target.value); }, [], ); const deleteThreadAction = React.useCallback(async () => { try { const response = await callDeleteThread(threadInfo.id, accountPassword); modalContext.popModal(); return response; } catch (e) { setErrorMessage( e.message === 'invalid_credentials' ? 'wrong password' : 'unknown error', ); setAccountPassword(''); accountPasswordInputRef.current?.focus(); throw e; } }, [ accountPassword, callDeleteThread, modalContext, setAccountPassword, setErrorMessage, threadInfo.id, ]); const onDelete = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatchActionPromise(deleteThreadActionTypes, deleteThreadAction()); }, [deleteThreadAction, dispatchActionPromise], ); return (

Your thread will be permanently deleted. There is no way to reverse this.

Please enter your account password to confirm your identity.

Account password
); } export default ThreadSettingsDeleteTab; diff --git a/web/modals/threads/thread-settings-general-tab.css b/web/modals/threads/settings/thread-settings-general-tab.css similarity index 100% rename from web/modals/threads/thread-settings-general-tab.css rename to web/modals/threads/settings/thread-settings-general-tab.css diff --git a/web/modals/threads/thread-settings-general-tab.react.js b/web/modals/threads/settings/thread-settings-general-tab.react.js similarity index 94% rename from web/modals/threads/thread-settings-general-tab.react.js rename to web/modals/threads/settings/thread-settings-general-tab.react.js index 85ac782dc..5d629c967 100644 --- a/web/modals/threads/thread-settings-general-tab.react.js +++ b/web/modals/threads/settings/thread-settings-general-tab.react.js @@ -1,179 +1,179 @@ // @flow import * as React from 'react'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; -import { type SetState } from 'lib/types/hook-types.js'; +import { type SetState } from 'lib/types/hook-types'; import { type ThreadInfo, type ThreadChanges } from 'lib/types/thread-types'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils'; import { firstLine } from 'lib/utils/string-utils'; -import Button from '../../components/button.react'; -import Input from '../input.react.js'; -import { useModalContext } from '../modal-provider.react.js'; -import ColorSelector from './color-selector.react.js'; +import Button from '../../../components/button.react'; +import Input from '../../input.react'; +import { useModalContext } from '../../modal-provider.react'; +import ColorSelector from '../color-selector.react'; import css from './thread-settings-general-tab.css'; type ThreadSettingsGeneralTabProps = { +inputDisabled: boolean, +threadInfo: ThreadInfo, +threadNamePlaceholder: string, +queuedChanges: ThreadChanges, +setQueuedChanges: SetState, +setErrorMessage: SetState, }; function ThreadSettingsGeneralTab( props: ThreadSettingsGeneralTabProps, ): React.Node { const { inputDisabled, threadInfo, threadNamePlaceholder, queuedChanges, setQueuedChanges, setErrorMessage, } = props; const modalContext = useModalContext(); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); const nameInputRef = React.useRef(); React.useEffect(() => { nameInputRef.current?.focus(); }, [inputDisabled]); const changeQueued: boolean = React.useMemo( () => Object.values(queuedChanges).some(v => v !== null && v !== undefined), [queuedChanges], ); const onChangeName = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, name: firstLine( target.value !== threadInfo.name ? target.value : undefined, ), }), ); }, [setQueuedChanges, threadInfo.name], ); const onChangeDescription = React.useCallback( (event: SyntheticEvent) => { const target = event.currentTarget; setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, description: target.value !== threadInfo.description ? target.value : undefined, }), ); }, [setQueuedChanges, threadInfo.description], ); const onChangeColor = React.useCallback( (color: string) => { setQueuedChanges(prevQueuedChanges => Object.freeze({ ...prevQueuedChanges, color: color !== threadInfo.color ? color : undefined, }), ); }, [setQueuedChanges, threadInfo.color], ); const changeThreadSettingsAction = React.useCallback(async () => { try { const response = await callChangeThreadSettings({ threadID: threadInfo.id, changes: queuedChanges, }); modalContext.popModal(); return response; } catch (e) { setErrorMessage('unknown_error'); setQueuedChanges(Object.freeze({})); throw e; } }, [ callChangeThreadSettings, modalContext, queuedChanges, setErrorMessage, setQueuedChanges, threadInfo.id, ]); const onSubmit = React.useCallback( (event: SyntheticEvent) => { event.preventDefault(); dispatchActionPromise( changeThreadSettingsActionTypes, changeThreadSettingsAction(), ); }, [changeThreadSettingsAction, dispatchActionPromise], ); return (
Thread name
Description