diff --git a/lib/shared/thread-settings-notifications-utils.js b/lib/shared/thread-settings-notifications-utils.js new file mode 100644 index 000000000..657b17abd --- /dev/null +++ b/lib/shared/thread-settings-notifications-utils.js @@ -0,0 +1,110 @@ +// @flow + +import * as React from 'react'; + +import { + updateSubscriptionActionTypes, + useUpdateSubscription, +} from '../actions/user-actions.js'; +import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; +import type { ThreadInfo } from '../types/minimally-encoded-thread-permissions-types.js'; +import { useDispatchActionPromise } from '../utils/redux-promise-utils.js'; +import { useSelector } from '../utils/redux-utils.js'; + +type NotificationSettings = 'focused' | 'badge-only' | 'background'; + +const updateSubscriptionLoadingStatusSelector = createLoadingStatusSelector( + updateSubscriptionActionTypes, +); + +function useThreadSettingsNotifications( + threadInfo: ThreadInfo, + onSuccessCallback: () => mixed, +): { + +notificationSettings: NotificationSettings, + +onFocusedSelected: () => mixed, + +onBadgeOnlySelected: () => mixed, + +onBackgroundSelected: () => mixed, + +saveButtonDisabled: boolean, + +onSave: () => mixed, +} { + const subscription = threadInfo.currentUser.subscription; + + const initialThreadSetting = React.useMemo(() => { + if (!subscription.home) { + return 'background'; + } + if (!subscription.pushNotifs) { + return 'badge-only'; + } + return 'focused'; + }, [subscription.home, subscription.pushNotifs]); + + const [notificationSettings, setNotificationSettings] = + React.useState(initialThreadSetting); + + const onFocusedSelected = React.useCallback( + () => setNotificationSettings('focused'), + [], + ); + const onBadgeOnlySelected = React.useCallback( + () => setNotificationSettings('badge-only'), + [], + ); + const onBackgroundSelected = React.useCallback( + () => setNotificationSettings('background'), + [], + ); + + const dispatchActionPromise = useDispatchActionPromise(); + + const callUpdateSubscription = useUpdateSubscription(); + + const updateSubscriptionPromise = React.useCallback(async () => { + const res = await callUpdateSubscription({ + threadID: threadInfo.id, + updatedFields: { + home: notificationSettings !== 'background', + pushNotifs: notificationSettings === 'focused', + }, + }); + + onSuccessCallback(); + + return res; + }, [ + callUpdateSubscription, + notificationSettings, + onSuccessCallback, + threadInfo.id, + ]); + + const updateSubscriptionLoadingStatus = useSelector( + updateSubscriptionLoadingStatusSelector, + ); + const isLoading = updateSubscriptionLoadingStatus === 'loading'; + const saveButtonDisabled = + isLoading || notificationSettings === initialThreadSetting; + + const onSave = React.useCallback(() => { + if (saveButtonDisabled) { + return; + } + + void dispatchActionPromise( + updateSubscriptionActionTypes, + updateSubscriptionPromise(), + ); + }, [saveButtonDisabled, dispatchActionPromise, updateSubscriptionPromise]); + + return { + notificationSettings, + onFocusedSelected, + onBadgeOnlySelected, + onBackgroundSelected, + saveButtonDisabled, + onSave, + }; +} + +export { useThreadSettingsNotifications }; diff --git a/web/components/enum-settings-option.react.js b/web/components/enum-settings-option.react.js index 5ff51e5bc..c5e8a92e5 100644 --- a/web/components/enum-settings-option.react.js +++ b/web/components/enum-settings-option.react.js @@ -1,120 +1,120 @@ // @flow import classnames from 'classnames'; import * as React from 'react'; import Checkbox from './checkbox.react.js'; import EnumSettingsOptionInfo from './enum-settings-option-info.react.js'; import css from './enum-settings-option.css'; import Radio from './radio.react.js'; const iconPositionClassnames = { top: css.optionIconTop, center: css.optionIconCenter, bottom: css.optionIconBottom, }; type InputType = 'radio' | 'checkbox'; type IconPosition = $Keys; type Props = { +selected: boolean, - +onSelect: () => void, + +onSelect: () => mixed, +disabled?: boolean, +icon?: React.Node, +title: string, +type?: InputType, +iconPosition?: IconPosition, +statements: $ReadOnlyArray<{ +statement: string, +isStatementValid?: boolean, +styleStatementBasedOnValidity?: boolean, }>, }; function EnumSettingsOption(props: Props): React.Node { const { icon, title, statements, selected, onSelect, disabled = false, type = 'radio', iconPosition = 'center', } = props; const descriptionItems = React.useMemo( () => statements.map( ({ statement, isStatementValid, styleStatementBasedOnValidity }) => ( {statement} ), ), [selected, statements], ); const inputIcon = React.useMemo(() => { if (disabled) { return null; } else if (type === 'checkbox') { return ; } else if (type === 'radio') { return ; } return undefined; }, [disabled, type, selected]); const optionContainerClasses = classnames(css.optionContainer, { [css.optionContainerSelected]: selected, }); const optionIconClasses = classnames( css.optionIcon, iconPositionClassnames[iconPosition], ); const optionIcon = React.useMemo(() => { if (!icon) { return null; } return
{icon}
; }, [icon, optionIconClasses]); const enumSettingsOption = React.useMemo( () => (
{optionIcon}
{title}
{descriptionItems}
{inputIcon}
), [ descriptionItems, disabled, inputIcon, onSelect, optionContainerClasses, optionIcon, title, ], ); return enumSettingsOption; } export default EnumSettingsOption; diff --git a/web/modals/threads/notifications/notifications-modal.react.js b/web/modals/threads/notifications/notifications-modal.react.js index 425887858..39782bdc1 100644 --- a/web/modals/threads/notifications/notifications-modal.react.js +++ b/web/modals/threads/notifications/notifications-modal.react.js @@ -1,300 +1,243 @@ // @flow import * as React from 'react'; -import { - useUpdateSubscription, - updateSubscriptionActionTypes, -} from 'lib/actions/user-actions.js'; import { useCanPromoteSidebar } from 'lib/hooks/promote-sidebar.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; +import { useThreadSettingsNotifications } from 'lib/shared/thread-settings-notifications-utils.js'; import { threadIsSidebar } from 'lib/shared/thread-utils.js'; -import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import css from './notifications-modal.css'; import AllNotifsIllustration from '../../../assets/all-notifs.react.js'; import BadgeNotifsIllustration from '../../../assets/badge-notifs.react.js'; import MutedNotifsIllustration from '../../../assets/muted-notifs.react.js'; import Button from '../../../components/button.react.js'; import EnumSettingsOption from '../../../components/enum-settings-option.react.js'; import { useSelector } from '../../../redux/redux-utils.js'; import Modal from '../../modal.react.js'; -type NotificationSettings = 'focused' | 'badge-only' | 'background'; - const BANNER_NOTIFS = 'Banner notifs'; const BADGE_COUNT = 'Badge count'; const IN_FOCUSED_TAB = 'Lives in Focused tab'; const IN_BACKGROUND_TAB = 'Lives in Background tab'; const focusedStatements = [ { statement: BANNER_NOTIFS, isStatementValid: true, styleStatementBasedOnValidity: true, }, { statement: BADGE_COUNT, isStatementValid: true, styleStatementBasedOnValidity: true, }, { statement: IN_FOCUSED_TAB, isStatementValid: true, styleStatementBasedOnValidity: true, }, ]; const badgeOnlyStatements = [ { statement: BANNER_NOTIFS, isStatementValid: false, styleStatementBasedOnValidity: true, }, { statement: BADGE_COUNT, isStatementValid: true, styleStatementBasedOnValidity: true, }, { statement: IN_FOCUSED_TAB, isStatementValid: true, styleStatementBasedOnValidity: true, }, ]; const backgroundStatements = [ { statement: BANNER_NOTIFS, isStatementValid: false, styleStatementBasedOnValidity: true, }, { statement: BADGE_COUNT, isStatementValid: false, styleStatementBasedOnValidity: true, }, { statement: IN_BACKGROUND_TAB, isStatementValid: true, styleStatementBasedOnValidity: true, }, ]; type Props = { +threadID: string, +onClose: () => void, }; function NotificationsModal(props: Props): React.Node { const { onClose, threadID } = props; const threadInfo = useSelector(state => threadInfoSelector(state)[threadID]); - const { subscription } = threadInfo.currentUser; const { parentThreadID } = threadInfo; const parentThreadInfo = useSelector(state => parentThreadID ? threadInfoSelector(state)[parentThreadID] : null, ); const isSidebar = threadIsSidebar(threadInfo); - const initialThreadSetting = React.useMemo(() => { - if (!subscription.home) { - return 'background'; - } - if (!subscription.pushNotifs) { - return 'badge-only'; - } - return 'focused'; - }, [subscription.home, subscription.pushNotifs]); - - const [notificationSettings, setNotificationSettings] = - React.useState(initialThreadSetting); - - const onFocusedSelected = React.useCallback( - () => setNotificationSettings('focused'), - [], - ); - const onBadgeOnlySelected = React.useCallback( - () => setNotificationSettings('badge-only'), - [], - ); - const onBackgroundSelected = React.useCallback( - () => setNotificationSettings('background'), - [], - ); + const { + notificationSettings, + onFocusedSelected, + onBadgeOnlySelected, + onBackgroundSelected, + saveButtonDisabled, + onSave, + } = useThreadSettingsNotifications(threadInfo, onClose); const isFocusedSelected = notificationSettings === 'focused'; const focusedItem = React.useMemo(() => { const icon = ; return ( ); }, [isFocusedSelected, onFocusedSelected]); const isFocusedBadgeOnlySelected = notificationSettings === 'badge-only'; const focusedBadgeOnlyItem = React.useMemo(() => { const icon = ; return ( ); }, [isFocusedBadgeOnlySelected, onBadgeOnlySelected]); const isBackgroundSelected = notificationSettings === 'background'; const backgroundItem = React.useMemo(() => { const icon = ; return ( ); }, [isBackgroundSelected, onBackgroundSelected, isSidebar]); - const dispatchActionPromise = useDispatchActionPromise(); - - const callUpdateSubscription = useUpdateSubscription(); - - const onClickSave = React.useCallback(() => { - void dispatchActionPromise( - updateSubscriptionActionTypes, - callUpdateSubscription({ - threadID: threadID, - updatedFields: { - home: notificationSettings !== 'background', - pushNotifs: notificationSettings === 'focused', - }, - }), - ); - onClose(); - }, [ - callUpdateSubscription, - dispatchActionPromise, - notificationSettings, - onClose, - threadID, - ]); - const modalName = isSidebar ? 'Thread notifications' : 'Channel notifications'; const canPromoteSidebar = useCanPromoteSidebar(threadInfo, parentThreadInfo); const noticeText = React.useMemo(() => { if (!isSidebar) { return null; } return ( <>

{'It’s not possible to move this thread to Background. ' + 'That’s because Comm’s design always shows threads ' + 'underneath their parent in the Inbox, which means ' + 'that if a thread’s parent is in Focused, the thread ' + 'must also be there.'}

{canPromoteSidebar ? 'If you want to move this thread to Background, ' + 'you can either move the parent to Background, ' + 'or you can promote the thread to a channel.' : 'If you want to move this thread to Background, ' + 'you’ll have to move the parent to Background.'}

); }, [isSidebar, canPromoteSidebar]); const parentThreadIsInBackground = isSidebar && !parentThreadInfo?.currentUser.subscription.home; const modalContent = React.useMemo(() => { if (parentThreadIsInBackground) { return ( <>

{'It’s not possible to change the notif settings for a thread ' + 'whose parent is in Background. That’s because Comm’s design ' + 'always shows threads underneath their parent in the Inbox, ' + 'which means that if a thread’s parent is in Background, the ' + 'thread must also be there.'}

{canPromoteSidebar ? 'If you want to change the notif settings for this thread, ' + 'you can either change the notif settings for the parent, ' + 'or you can promote the thread to a channel.' : 'If you want to change the notif settings for this thread, ' + 'you’ll have to change the notif settings for the parent.'}

); } return ( <>
{focusedItem} {focusedBadgeOnlyItem} {backgroundItem}
{noticeText} ); }, [ backgroundItem, focusedBadgeOnlyItem, focusedItem, noticeText, parentThreadIsInBackground, canPromoteSidebar, ]); const saveButton = React.useMemo(() => { if (parentThreadIsInBackground) { return undefined; } return ( - ); - }, [ - initialThreadSetting, - notificationSettings, - onClickSave, - parentThreadIsInBackground, - ]); + }, [saveButtonDisabled, onSave, parentThreadIsInBackground]); return (
{modalContent}
); } export default NotificationsModal;