diff --git a/lib/actions/user-actions.js b/lib/actions/user-actions.js index a10685926..37b1c8d9f 100644 --- a/lib/actions/user-actions.js +++ b/lib/actions/user-actions.js @@ -1,375 +1,407 @@ // @flow import threadWatcher from '../shared/thread-watcher.js'; import type { LogOutResult, LogInInfo, LogInResult, RegisterResult, RegisterInfo, UpdateUserSettingsRequest, PolicyAcknowledgmentRequest, ClaimUsernameResponse, } from '../types/account-types.js'; import type { UpdateUserAvatarRequest, UpdateUserAvatarResponse, } from '../types/avatar-types.js'; import type { GetSessionPublicKeysArgs, GetOlmSessionInitializationDataResponse, } from '../types/request-types.js'; import type { UserSearchResult, ExactUserSearchResult, } from '../types/search-types.js'; import type { SessionPublicKeys, PreRequestUserState, } from '../types/session-types.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from '../types/subscription-types.js'; import type { UserInfo, PasswordUpdate } from '../types/user-types.js'; +import { extractKeyserverIDFromID } from '../utils/action-utils.js'; import type { CallServerEndpoint, CallServerEndpointOptions, } from '../utils/call-server-endpoint.js'; import { getConfig } from '../utils/config.js'; +import type { CallKeyserverEndpoint } from '../utils/keyserver-call'; +import { useKeyserverCall } from '../utils/keyserver-call.js'; import sleep from '../utils/sleep.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; const logOutActionTypes = Object.freeze({ started: 'LOG_OUT_STARTED', success: 'LOG_OUT_SUCCESS', failed: 'LOG_OUT_FAILED', }); const logOut = ( callServerEndpoint: CallServerEndpoint, ): ((preRequestUserState: PreRequestUserState) => Promise) => async preRequestUserState => { let response = null; try { response = await Promise.race([ callServerEndpoint('log_out', {}), (async () => { await sleep(500); throw new Error('log_out took more than 500ms'); })(), ]); } catch {} const currentUserInfo = response ? response.currentUserInfo : null; return { currentUserInfo, preRequestUserState }; }; const claimUsernameActionTypes = Object.freeze({ started: 'CLAIM_USERNAME_STARTED', success: 'CLAIM_USERNAME_SUCCESS', failed: 'CLAIM_USERNAME_FAILED', }); const claimUsernameCallServerEndpointOptions = { timeout: 500 }; const claimUsername = ( - callServerEndpoint: CallServerEndpoint, + callKeyserverEndpoint: CallKeyserverEndpoint, ): (() => Promise) => async () => { - const response = await callServerEndpoint( - 'claim_username', - {}, - { ...claimUsernameCallServerEndpointOptions }, - ); - return response; + const requests = { [ashoatKeyserverID]: {} }; + const responses = await callKeyserverEndpoint('claim_username', requests, { + ...claimUsernameCallServerEndpointOptions, + }); + const response = responses[ashoatKeyserverID]; + return { + message: response.message, + signature: response.signature, + }; }; +function useClaimUsername(): () => Promise { + return useKeyserverCall(claimUsername); +} + const deleteAccountActionTypes = Object.freeze({ started: 'DELETE_ACCOUNT_STARTED', success: 'DELETE_ACCOUNT_SUCCESS', failed: 'DELETE_ACCOUNT_FAILED', }); const deleteAccount = ( callServerEndpoint: CallServerEndpoint, ): ((preRequestUserState: PreRequestUserState) => Promise) => async preRequestUserState => { const response = await callServerEndpoint('delete_account'); return { currentUserInfo: response.currentUserInfo, preRequestUserState }; }; const registerActionTypes = Object.freeze({ started: 'REGISTER_STARTED', success: 'REGISTER_SUCCESS', failed: 'REGISTER_FAILED', }); const registerCallServerEndpointOptions = { timeout: 60000 }; const register = ( callServerEndpoint: CallServerEndpoint, ): (( registerInfo: RegisterInfo, options?: CallServerEndpointOptions, ) => Promise) => async (registerInfo, options) => { const response = await callServerEndpoint( 'create_account', { ...registerInfo, platformDetails: getConfig().platformDetails, }, { ...registerCallServerEndpointOptions, ...options, }, ); return { currentUserInfo: response.currentUserInfo, rawMessageInfos: response.rawMessageInfos, threadInfos: response.cookieChange.threadInfos, userInfos: response.cookieChange.userInfos, calendarQuery: registerInfo.calendarQuery, }; }; function mergeUserInfos(...userInfoArrays: UserInfo[][]): UserInfo[] { const merged = {}; for (const userInfoArray of userInfoArrays) { for (const userInfo of userInfoArray) { merged[userInfo.id] = userInfo; } } const flattened = []; for (const id in merged) { flattened.push(merged[id]); } return flattened; } const logInActionTypes = Object.freeze({ started: 'LOG_IN_STARTED', success: 'LOG_IN_SUCCESS', failed: 'LOG_IN_FAILED', }); const logInCallServerEndpointOptions = { timeout: 60000 }; const logIn = ( callServerEndpoint: CallServerEndpoint, ): ((logInInfo: LogInInfo) => Promise) => async logInInfo => { const watchedIDs = threadWatcher.getWatchedIDs(); const { logInActionSource, ...restLogInInfo } = logInInfo; const response = await callServerEndpoint( 'log_in', { ...restLogInInfo, source: logInActionSource, watchedIDs, platformDetails: getConfig().platformDetails, }, logInCallServerEndpointOptions, ); const userInfos = mergeUserInfos( response.userInfos, response.cookieChange.userInfos, ); return { threadInfos: response.cookieChange.threadInfos, currentUserInfo: response.currentUserInfo, calendarResult: { calendarQuery: logInInfo.calendarQuery, rawEntryInfos: response.rawEntryInfos, }, messagesResult: { messageInfos: response.rawMessageInfos, truncationStatus: response.truncationStatuses, watchedIDsAtRequestTime: watchedIDs, currentAsOf: response.serverTime, }, userInfos, updatesCurrentAsOf: response.serverTime, logInActionSource: logInInfo.logInActionSource, notAcknowledgedPolicies: response.notAcknowledgedPolicies, }; }; const changeUserPasswordActionTypes = Object.freeze({ started: 'CHANGE_USER_PASSWORD_STARTED', success: 'CHANGE_USER_PASSWORD_SUCCESS', failed: 'CHANGE_USER_PASSWORD_FAILED', }); const changeUserPassword = ( callServerEndpoint: CallServerEndpoint, ): ((passwordUpdate: PasswordUpdate) => Promise) => async passwordUpdate => { await callServerEndpoint('update_account', passwordUpdate); }; const searchUsersActionTypes = Object.freeze({ started: 'SEARCH_USERS_STARTED', success: 'SEARCH_USERS_SUCCESS', failed: 'SEARCH_USERS_FAILED', }); const searchUsers = ( callServerEndpoint: CallServerEndpoint, ): ((usernamePrefix: string) => Promise) => async usernamePrefix => { const response = await callServerEndpoint('search_users', { prefix: usernamePrefix, }); return { userInfos: response.userInfos, }; }; const exactSearchUserActionTypes = Object.freeze({ started: 'EXACT_SEARCH_USER_STARTED', success: 'EXACT_SEARCH_USER_SUCCESS', failed: 'EXACT_SEARCH_USER_FAILED', }); const exactSearchUser = ( callServerEndpoint: CallServerEndpoint, ): ((username: string) => Promise) => async username => { const response = await callServerEndpoint('exact_search_user', { username, }); return { userInfo: response.userInfo, }; }; const updateSubscriptionActionTypes = Object.freeze({ started: 'UPDATE_SUBSCRIPTION_STARTED', success: 'UPDATE_SUBSCRIPTION_SUCCESS', failed: 'UPDATE_SUBSCRIPTION_FAILED', }); const updateSubscription = ( - callServerEndpoint: CallServerEndpoint, + callKeyserverEndpoint: CallKeyserverEndpoint, ): (( - subscriptionUpdate: SubscriptionUpdateRequest, + input: SubscriptionUpdateRequest, ) => Promise) => - async subscriptionUpdate => { - const response = await callServerEndpoint( + async input => { + const keyserverID = extractKeyserverIDFromID(input.threadID); + const requests = { [keyserverID]: input }; + + const responses = await callKeyserverEndpoint( 'update_user_subscription', - subscriptionUpdate, + requests, ); + const response = responses[keyserverID]; return { - threadID: subscriptionUpdate.threadID, + threadID: input.threadID, subscription: response.threadSubscription, }; }; +function useUpdateSubscription(): ( + input: SubscriptionUpdateRequest, +) => Promise { + return useKeyserverCall(updateSubscription); +} + const setUserSettingsActionTypes = Object.freeze({ started: 'SET_USER_SETTINGS_STARTED', success: 'SET_USER_SETTINGS_SUCCESS', failed: 'SET_USER_SETTINGS_FAILED', }); const setUserSettings = ( - callServerEndpoint: CallServerEndpoint, - ): ((userSettingsRequest: UpdateUserSettingsRequest) => Promise) => - async userSettingsRequest => { - await callServerEndpoint('update_user_settings', userSettingsRequest); + callKeyserverEndpoint: CallKeyserverEndpoint, + allKeyserverIDs: $ReadOnlyArray, + ): ((input: UpdateUserSettingsRequest) => Promise) => + async input => { + const requests = {}; + for (const keyserverID of allKeyserverIDs) { + requests[keyserverID] = input; + } + await callKeyserverEndpoint('update_user_settings', requests); }; +function useSetUserSettings(): ( + input: UpdateUserSettingsRequest, +) => Promise { + return useKeyserverCall(setUserSettings); +} + const getSessionPublicKeys = ( callServerEndpoint: CallServerEndpoint, ): ((data: GetSessionPublicKeysArgs) => Promise) => async data => { return await callServerEndpoint('get_session_public_keys', data); }; const getOlmSessionInitializationDataActionTypes = Object.freeze({ started: 'GET_OLM_SESSION_INITIALIZATION_DATA_STARTED', success: 'GET_OLM_SESSION_INITIALIZATION_DATA_SUCCESS', failed: 'GET_OLM_SESSION_INITIALIZATION_DATA_FAILED', }); const getOlmSessionInitializationData = ( callServerEndpoint: CallServerEndpoint, ): (( options?: ?CallServerEndpointOptions, ) => Promise) => async options => { return await callServerEndpoint( 'get_olm_session_initialization_data', {}, options, ); }; const policyAcknowledgmentActionTypes = Object.freeze({ started: 'POLICY_ACKNOWLEDGMENT_STARTED', success: 'POLICY_ACKNOWLEDGMENT_SUCCESS', failed: 'POLICY_ACKNOWLEDGMENT_FAILED', }); const policyAcknowledgment = ( callServerEndpoint: CallServerEndpoint, ): ((policyRequest: PolicyAcknowledgmentRequest) => Promise) => async policyRequest => { await callServerEndpoint('policy_acknowledgment', policyRequest); }; const updateUserAvatarActionTypes = Object.freeze({ started: 'UPDATE_USER_AVATAR_STARTED', success: 'UPDATE_USER_AVATAR_SUCCESS', failed: 'UPDATE_USER_AVATAR_FAILED', }); const updateUserAvatar = ( callServerEndpoint: CallServerEndpoint, ): (( avatarDBContent: UpdateUserAvatarRequest, ) => Promise) => async avatarDBContent => { const { updates }: UpdateUserAvatarResponse = await callServerEndpoint( 'update_user_avatar', avatarDBContent, ); return { updates }; }; const resetUserStateActionType = 'RESET_USER_STATE'; const setAccessTokenActionType = 'SET_ACCESS_TOKEN'; export { changeUserPasswordActionTypes, changeUserPassword, claimUsernameActionTypes, - claimUsername, + useClaimUsername, deleteAccount, deleteAccountActionTypes, getSessionPublicKeys, getOlmSessionInitializationDataActionTypes, getOlmSessionInitializationData, mergeUserInfos, logIn, logInActionTypes, logOut, logOutActionTypes, register, registerActionTypes, searchUsers, searchUsersActionTypes, exactSearchUser, exactSearchUserActionTypes, - setUserSettings, + useSetUserSettings, setUserSettingsActionTypes, - updateSubscription, + useUpdateSubscription, updateSubscriptionActionTypes, policyAcknowledgment, policyAcknowledgmentActionTypes, updateUserAvatarActionTypes, updateUserAvatar, resetUserStateActionType, setAccessTokenActionType, }; diff --git a/native/chat/settings/thread-settings-home-notifs.react.js b/native/chat/settings/thread-settings-home-notifs.react.js index 49fb561b7..2a83fa20c 100644 --- a/native/chat/settings/thread-settings-home-notifs.react.js +++ b/native/chat/settings/thread-settings-home-notifs.react.js @@ -1,119 +1,116 @@ // @flow import * as React from 'react'; import { View, Switch } from 'react-native'; import { updateSubscriptionActionTypes, - updateSubscription, + useUpdateSubscription, } from 'lib/actions/user-actions.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from 'lib/types/subscription-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import type { DispatchActionPromise } from 'lib/utils/action-utils.js'; -import { - useServerCall, - useDispatchActionPromise, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import SingleLine from '../../components/single-line.react.js'; import { useStyles } from '../../themes/colors.js'; type BaseProps = { +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, // Redux state +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +updateSubscription: ( subscriptionUpdate: SubscriptionUpdateRequest, ) => Promise, }; type State = { +currentValue: boolean, }; class ThreadSettingsHomeNotifs extends React.PureComponent { constructor(props: Props) { super(props); this.state = { currentValue: !props.threadInfo.currentUser.subscription.home, }; } render() { const componentLabel = 'Background'; return ( {componentLabel} ); } onValueChange = (value: boolean) => { this.setState({ currentValue: value }); this.props.dispatchActionPromise( updateSubscriptionActionTypes, this.props.updateSubscription({ threadID: this.props.threadInfo.id, updatedFields: { home: !value, }, }), ); }; } const unboundStyles = { currentValue: { alignItems: 'flex-end', margin: 0, paddingLeft: 4, paddingRight: 0, paddingVertical: 0, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, flex: 1, }, row: { alignItems: 'center', backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 3, }, }; const ConnectedThreadSettingsHomeNotifs: React.ComponentType = React.memo(function ConnectedThreadSettingsHomeNotifs( props: BaseProps, ) { const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); - const callUpdateSubscription = useServerCall(updateSubscription); + const callUpdateSubscription = useUpdateSubscription(); return ( ); }); export default ConnectedThreadSettingsHomeNotifs; diff --git a/native/chat/settings/thread-settings-push-notifs.react.js b/native/chat/settings/thread-settings-push-notifs.react.js index 87d94fdf4..26ecd034f 100644 --- a/native/chat/settings/thread-settings-push-notifs.react.js +++ b/native/chat/settings/thread-settings-push-notifs.react.js @@ -1,194 +1,191 @@ // @flow import * as React from 'react'; import { View, Switch, TouchableOpacity, Platform } from 'react-native'; import Linking from 'react-native/Libraries/Linking/Linking.js'; import { updateSubscriptionActionTypes, - updateSubscription, + useUpdateSubscription, } from 'lib/actions/user-actions.js'; import type { SubscriptionUpdateRequest, SubscriptionUpdateResult, } from 'lib/types/subscription-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; import type { DispatchActionPromise } from 'lib/utils/action-utils.js'; -import { - useServerCall, - useDispatchActionPromise, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-utils.js'; import SingleLine from '../../components/single-line.react.js'; import SWMansionIcon from '../../components/swmansion-icon.react.js'; import { CommAndroidNotifications } from '../../push/android.js'; import { useSelector } from '../../redux/redux-utils.js'; import { useStyles } from '../../themes/colors.js'; import Alert from '../../utils/alert.js'; type BaseProps = { +threadInfo: ThreadInfo, }; type Props = { ...BaseProps, // Redux state +styles: typeof unboundStyles, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +hasPushPermissions: boolean, +updateSubscription: ( subscriptionUpdate: SubscriptionUpdateRequest, ) => Promise, }; type State = { +currentValue: boolean, }; class ThreadSettingsPushNotifs extends React.PureComponent { constructor(props: Props) { super(props); this.state = { currentValue: props.threadInfo.currentUser.subscription.pushNotifs, }; } render() { const componentLabel = 'Push notifs'; let notificationsSettingsLinkingIcon = undefined; if (!this.props.hasPushPermissions) { notificationsSettingsLinkingIcon = ( ); } return ( {componentLabel} {notificationsSettingsLinkingIcon} ); } onValueChange = (value: boolean) => { this.setState({ currentValue: value }); this.props.dispatchActionPromise( updateSubscriptionActionTypes, this.props.updateSubscription({ threadID: this.props.threadInfo.id, updatedFields: { pushNotifs: value, }, }), ); }; onNotificationsSettingsLinkingIconPress = async () => { let platformRequestsPermission; if (Platform.OS !== 'android') { platformRequestsPermission = true; } else { platformRequestsPermission = await CommAndroidNotifications.canRequestNotificationsPermissionFromUser(); } const alertTitle = platformRequestsPermission ? 'Need notif permissions' : 'Unable to initialize notifs'; const notificationsSettingsPath = Platform.OS === 'ios' ? 'Settings App → Notifications → Comm' : 'Settings → Apps → Comm → Notifications'; let alertMessage; if (platformRequestsPermission && this.state.currentValue) { alertMessage = 'Notifs for this chat are enabled, but cannot be delivered ' + 'to this device because you haven’t granted notif permissions to Comm. ' + 'Please enable them in ' + notificationsSettingsPath; } else if (platformRequestsPermission) { alertMessage = 'In order to enable push notifs for this chat, ' + 'you need to first grant notif permissions to Comm. ' + 'Please enable them in ' + notificationsSettingsPath; } else { alertMessage = 'Please check your network connection, make sure Google Play ' + 'services are installed and enabled, and confirm that your Google ' + 'Play credentials are valid in the Google Play Store.'; } Alert.alert(alertTitle, alertMessage, [ { text: 'Go to settings', onPress: () => Linking.openSettings(), }, { text: 'Cancel', style: 'cancel', }, ]); }; } const unboundStyles = { currentValue: { alignItems: 'flex-end', margin: 0, paddingLeft: 4, paddingRight: 0, paddingVertical: 0, }, label: { color: 'panelForegroundTertiaryLabel', fontSize: 16, flex: 1, }, row: { alignItems: 'center', backgroundColor: 'panelForeground', flexDirection: 'row', paddingHorizontal: 24, paddingVertical: 3, }, infoIcon: { paddingRight: 20, }, }; const ConnectedThreadSettingsPushNotifs: React.ComponentType = React.memo(function ConnectedThreadSettingsPushNotifs( props: BaseProps, ) { const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); - const callUpdateSubscription = useServerCall(updateSubscription); + const callUpdateSubscription = useUpdateSubscription(); const hasPushPermissions = useSelector( state => state.deviceToken !== null && state.deviceToken !== undefined, ); return ( ); }); export default ConnectedThreadSettingsPushNotifs; diff --git a/native/profile/default-notifications-preferences.react.js b/native/profile/default-notifications-preferences.react.js index 072db24e1..482f3d4b1 100644 --- a/native/profile/default-notifications-preferences.react.js +++ b/native/profile/default-notifications-preferences.react.js @@ -1,214 +1,213 @@ // @flow import * as React from 'react'; import { View, Text, Platform } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { - setUserSettings, + useSetUserSettings, setUserSettingsActionTypes, } from 'lib/actions/user-actions.js'; import { registerFetchKey } from 'lib/reducers/loading-reducer.js'; import { type UpdateUserSettingsRequest, type NotificationTypes, type DefaultNotificationPayload, notificationTypes, userSettingsTypes, } from 'lib/types/account-types.js'; import { type DispatchActionPromise, - useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import type { ProfileNavigationProp } from './profile.react.js'; import Action from '../components/action-row.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; const CheckIcon = () => ( ); type ProfileRowProps = { +content: string, +onPress: () => void, +danger?: boolean, +selected?: boolean, }; function NotificationRow(props: ProfileRowProps): React.Node { const { content, onPress, danger, selected } = props; return ( {selected ? : null} ); } type BaseProps = { +navigation: ProfileNavigationProp<>, +route: NavigationRoute<'DefaultNotifications'>, }; type Props = { ...BaseProps, +styles: typeof unboundStyles, +dispatchActionPromise: DispatchActionPromise, +changeNotificationSettings: ( notificationSettingsRequest: UpdateUserSettingsRequest, ) => Promise, +selectedDefaultNotification: NotificationTypes, }; class DefaultNotificationsPreferences extends React.PureComponent { async updatedDefaultNotifications( data: NotificationTypes, ): Promise { const { changeNotificationSettings } = this.props; try { await changeNotificationSettings({ name: userSettingsTypes.DEFAULT_NOTIFICATIONS, data, }); } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: () => {} }], { cancelable: false }, ); } return { [userSettingsTypes.DEFAULT_NOTIFICATIONS]: data, }; } selectNotificationSetting = (data: NotificationTypes) => { const { dispatchActionPromise } = this.props; dispatchActionPromise( setUserSettingsActionTypes, this.updatedDefaultNotifications(data), ); }; selectAllNotifications = () => { this.selectNotificationSetting(notificationTypes.FOCUSED); }; selectBackgroundNotifications = () => { this.selectNotificationSetting(notificationTypes.BACKGROUND); }; selectNoneNotifications = () => { this.selectNotificationSetting(notificationTypes.BADGE_ONLY); }; render() { const { styles, selectedDefaultNotification } = this.props; return ( NOTIFICATIONS ); } } const unboundStyles = { scrollView: { backgroundColor: 'panelBackground', }, scrollViewContentContainer: { paddingTop: 24, }, section: { backgroundColor: 'panelForeground', borderBottomWidth: 1, borderColor: 'panelForegroundBorder', borderTopWidth: 1, marginBottom: 24, marginVertical: 2, }, icon: { lineHeight: Platform.OS === 'ios' ? 18 : 20, }, header: { color: 'panelBackgroundLabel', fontSize: 12, fontWeight: '400', paddingBottom: 3, paddingHorizontal: 24, }, }; registerFetchKey(setUserSettingsActionTypes); const ConnectedDefaultNotificationPreferences: React.ComponentType = React.memo(function ConnectedDefaultNotificationPreferences( props: BaseProps, ) { const styles = useStyles(unboundStyles); const dispatchActionPromise = useDispatchActionPromise(); - const changeNotificationSettings = useServerCall(setUserSettings); + const changeNotificationSettings = useSetUserSettings(); const defaultNotification = userSettingsTypes.DEFAULT_NOTIFICATIONS; const selectedDefaultNotification = useSelector( ({ currentUserInfo }) => { if ( currentUserInfo?.settings && currentUserInfo?.settings[defaultNotification] ) { return currentUserInfo?.settings[defaultNotification]; } return notificationTypes.FOCUSED; }, ); return ( ); }); export default ConnectedDefaultNotificationPreferences; diff --git a/web/modals/threads/notifications/notifications-modal.react.js b/web/modals/threads/notifications/notifications-modal.react.js index 0c70fe0d7..fe02430aa 100644 --- a/web/modals/threads/notifications/notifications-modal.react.js +++ b/web/modals/threads/notifications/notifications-modal.react.js @@ -1,269 +1,266 @@ // @flow import * as React from 'react'; import { - updateSubscription, + useUpdateSubscription, updateSubscriptionActionTypes, } from 'lib/actions/user-actions.js'; import { canPromoteSidebar } from 'lib/hooks/promote-sidebar.react.js'; import { threadInfoSelector } from 'lib/selectors/thread-selectors.js'; import { threadIsSidebar } from 'lib/shared/thread-utils.js'; -import { - useServerCall, - useDispatchActionPromise, -} from 'lib/utils/action-utils.js'; +import { useDispatchActionPromise } from 'lib/utils/action-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 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 = useServerCall(updateSubscription); + const callUpdateSubscription = useUpdateSubscription(); const onClickSave = React.useCallback(() => { 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'; let modalContent; if (isSidebar && !parentThreadInfo?.currentUser.subscription.home) { modalContent = ( <>

{'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(threadInfo, parentThreadInfo) ? '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.'}

); } else { let noticeText = null; if (isSidebar) { noticeText = ( <>

{'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(threadInfo, parentThreadInfo) ? '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.'}

); } modalContent = ( <>
{focusedItem} {focusedBadgeOnlyItem} {backgroundItem}
{noticeText} ); } return (
{modalContent}
); } export default NotificationsModal;