From be83201068dae5b9ebf152879c40c4bfb4edd962 Mon Sep 17 00:00:00 2001 From: "Andrew W. Hill" Date: Thu, 4 Apr 2019 12:27:02 -0700 Subject: [PATCH] more notification control (#1013) * adds toggles for verboseui options --- App/Config/ReduxPersist.ts | 16 +- App/Containers/RootContainer.tsx | 6 +- App/Redux/PreferencesRedux.ts | 32 ++- .../__snapshots__/PreferencesRedux.ts.snap | 5 + App/SB/views/DeviceLogs/index.js | 4 +- App/SB/views/Notifications/index.js | 154 ----------- App/SB/views/Notifications/index.tsx | 247 ++++++++++++++++++ App/Sagas/ImageSharingSagas.ts | 2 + App/Sagas/NotificationsSagas.ts | 9 +- App/Sagas/PhotoViewingSagas.ts | 6 + App/Sagas/TextileEventsSagas.ts | 13 +- 11 files changed, 323 insertions(+), 171 deletions(-) delete mode 100644 App/SB/views/Notifications/index.js create mode 100644 App/SB/views/Notifications/index.tsx diff --git a/App/Config/ReduxPersist.ts b/App/Config/ReduxPersist.ts index 6b5634347..97830d1d4 100644 --- a/App/Config/ReduxPersist.ts +++ b/App/Config/ReduxPersist.ts @@ -310,13 +310,27 @@ const migrations: MigrationManifest = { const state = persistedState as any const { migration, ...rest } = state return rest + }, + 20: (persistedState) => { + const state = persistedState as any + return { + ...state, + preferences: { + ...state.preferences, + verboseUiOptions: { + nodeStateOverlay: true, + nodeStateNotifications: true, + nodeErrorNotifications: true + } + } + } } } const persistConfig: PersistConfig = { key: 'primary', storage: AsyncStorage, - version: 19, + version: 20, whitelist: ['account', 'preferences', 'uploadingImages', 'group', 'cameraRoll', 'storage', 'deviceLogs'], migrate: createMigrate(migrations, { debug: false }) } diff --git a/App/Containers/RootContainer.tsx b/App/Containers/RootContainer.tsx index c54e4998b..603157a6d 100644 --- a/App/Containers/RootContainer.tsx +++ b/App/Containers/RootContainer.tsx @@ -15,7 +15,7 @@ import styles from './Styles/RootContainerStyles' interface StateProps { monitorLocation: boolean nodeState: string - verboseUi: boolean + showVerboseUi: boolean } interface DispatchProps { @@ -62,7 +62,7 @@ class RootContainer extends Component { { NavigationService.setTopLevelNavigator(navRef) }} /> - {this.props.verboseUi && + {this.props.showVerboseUi && {overlayMessage} @@ -75,7 +75,7 @@ class RootContainer extends Component { const mapStateToProps = (state: RootState): StateProps => { return { monitorLocation: state.preferences.services.backgroundLocation.status, - verboseUi: state.preferences.verboseUi, + showVerboseUi: state.preferences.verboseUi && state.preferences.verboseUiOptions.nodeStateOverlay, nodeState: state.textile.nodeState.state } } diff --git a/App/Redux/PreferencesRedux.ts b/App/Redux/PreferencesRedux.ts index 69e74235a..300f9d057 100644 --- a/App/Redux/PreferencesRedux.ts +++ b/App/Redux/PreferencesRedux.ts @@ -19,6 +19,16 @@ const actions = { }), updateViewSetting: createAction('TOGGLE_VIEW_SETTING', (resolve) => { return (name: string, value: string) => resolve({ name, value }) + }), + // Verbose UI options + toggleNodeStateNotifications: createAction('TOGGLE_NODE_STATE_NOTIFICATIONS', (resolve) => { + return () => resolve() + }), + toggleNodeStateOverlay: createAction('TOGGLE_NODE_STATE_OVERLAY', (resolve) => { + return () => resolve() + }), + toggleNodeErrorNotifications: createAction('TOGGLE_NODE_ERROR_NOTIFICATIONS', (resolve) => { + return () => resolve() }) } @@ -45,17 +55,21 @@ export interface ViewSettings { selectedWalletTab: 'Photos' | 'Groups' | 'Contacts', } export interface PreferencesState { - verboseUi: boolean readonly services: {readonly [k in ServiceType]: Service} readonly storage: {readonly [k in StorageType]: Service} readonly tourScreens: {readonly [k in TourScreens]: boolean} // true = still need to show, false = no need viewSettings: ViewSettings onboarded: boolean + verboseUi: boolean + verboseUiOptions: { + nodeStateOverlay: boolean + nodeStateNotifications: boolean + nodeErrorNotifications: boolean + } } export const initialState: PreferencesState = { onboarded: false, - verboseUi: false, tourScreens: { wallet: true, threads: true, @@ -113,11 +127,23 @@ export const initialState: PreferencesState = { }, viewSettings: { selectedWalletTab: 'Groups' + }, + verboseUi: false, + verboseUiOptions: { + nodeStateOverlay: true, + nodeStateNotifications: true, + nodeErrorNotifications: true } } export function reducer (state: PreferencesState = initialState, action: PreferencesAction): PreferencesState { switch (action.type) { + case getType(actions.toggleNodeErrorNotifications): + return { ...state, verboseUiOptions: {...state.verboseUiOptions, nodeErrorNotifications: !state.verboseUiOptions.nodeErrorNotifications} } + case getType(actions.toggleNodeStateNotifications): + return { ...state, verboseUiOptions: {...state.verboseUiOptions, nodeStateNotifications: !state.verboseUiOptions.nodeStateNotifications} } + case getType(actions.toggleNodeStateOverlay): + return { ...state, verboseUiOptions: {...state.verboseUiOptions, nodeStateOverlay: !state.verboseUiOptions.nodeStateOverlay} } case getType(actions.onboardingSuccess): return { ...state, onboarded: true } case getType(actions.toggleVerboseUi): @@ -147,6 +173,8 @@ export const PreferencesSelectors = { service: (state: RootState, name: ServiceType) => state.preferences.services[name], storage: (state: RootState, name: StorageType) => state.preferences.storage[name], verboseUi: (state: RootState) => state.preferences.verboseUi, + showNodeStateNotification: (state: RootState) => state.preferences.verboseUi && state.preferences.verboseUiOptions.nodeStateNotifications, + showNodeErrorNotification: (state: RootState) => state.preferences.verboseUi && state.preferences.verboseUiOptions.nodeErrorNotifications, autoPinStatus: (state: RootState) => state.preferences.storage.autoPinPhotos.status, showNotificationPrompt: (state: RootState) => state.preferences.tourScreens.notifications, showBackgroundLocationPrompt: (state: RootState) => { diff --git a/App/Redux/__tests__/__snapshots__/PreferencesRedux.ts.snap b/App/Redux/__tests__/__snapshots__/PreferencesRedux.ts.snap index ffd4417b5..38d1af646 100644 --- a/App/Redux/__tests__/__snapshots__/PreferencesRedux.ts.snap +++ b/App/Redux/__tests__/__snapshots__/PreferencesRedux.ts.snap @@ -59,6 +59,11 @@ Object { "wallet": true, }, "verboseUi": false, + "verboseUiOptions": Object { + "nodeErrorNotifications": true, + "nodeStateNotifications": true, + "nodeStateOverlay": true, + }, "viewSettings": Object { "selectedWalletTab": "Groups", }, diff --git a/App/SB/views/DeviceLogs/index.js b/App/SB/views/DeviceLogs/index.js index 5723ae5c8..233eb33a9 100644 --- a/App/SB/views/DeviceLogs/index.js +++ b/App/SB/views/DeviceLogs/index.js @@ -71,10 +71,10 @@ class DeviceLogs extends React.PureComponent { {moment(item.time).format('LTS')} - {item.event} + {item.event} - {item.message} + {item.message} ) diff --git a/App/SB/views/Notifications/index.js b/App/SB/views/Notifications/index.js deleted file mode 100644 index 0a739a859..000000000 --- a/App/SB/views/Notifications/index.js +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react' -import { connect } from 'react-redux' -import { View, Text, ScrollView, Platform } from 'react-native' -import PreferencesActions from '../../../Redux/PreferencesRedux' -import PermissionsInfo from '../../components/PermissionsInfo' -import HeaderButtons, { Item } from 'react-navigation-header-buttons' -import SettingsRow from '../../components/SettingsRow' -import GetServiceInfo from './GetServiceInfo' - -import { TextileHeaderButtons, Item as TextileItem } from '../../../Components/HeaderButtons' - -import styles from './statics/styles' -import Avatar from '../../../Components/Avatar' -import { NavigationActions } from 'react-navigation' - -class Notifications extends React.PureComponent { - constructor (props) { - super(props) - this.state = { - complete: false, - iOS: Platform.OS === 'ios', - cameraRoll: false, - locationBackground: false, - infoVisible: false, - info: { } - } - } - - static navigationOptions = ({ navigation }) => { - return { - headerTitle: 'Notifications', - headerLeft: ( - - { navigation.dispatch(NavigationActions.back()) }} /> - - ), - headerRight: ( - - - } - /> - - ) - } - } - - toggleService (name) { - if (name === 'notifications') { - // never prompt the user later to get those - this.props.completeScreen(name) - } - this.props.toggleServicesRequest(name) - } - - hideInfo () { - this.setState({ infoVisible: false }) - } - - showInfo (service) { - const info = GetServiceInfo(service) - this.setState({ infoVisible: true, info }) - } - - render () { - return ( - - - Choose the types of notifications you want to receive. - - - - {Object.keys(this.props.services) - .map((service, i) => { - const value = !!this.props.services[service].status - let children = Object.keys(this.props.children) - .filter((key) => this.props.children[key].info.dependsOn === service) - .reduce((previous, current) => { - previous[current] = this.props.children[current] - return previous - }, {}) - - return ( - - - {children && Object.keys(children).map((child, i) => - - )} - - ) - } - )} - - - {this.state.infoVisible && } - - ) - } -} - -const mapStateToProps = state => { - // get all top level services - const allServices = Object.keys(state.preferences.services) - .reduce((previous, current) => { - let basic = { - status: state.preferences.services[current].status - } - basic.info = GetServiceInfo(current) - previous[current] = basic - return previous - }, {}) - - const services = Object.keys(allServices) - .filter((key) => allServices[key].info !== undefined && allServices[key].info.dependsOn === undefined) - .reduce((previous, current) => { - previous[current] = allServices[current] - return previous - }, {}) - - // get any services that depend on top level services - const children = Object.keys(allServices) - .filter((key) => allServices[key].info !== undefined && allServices[key].info.dependsOn !== undefined) - .reduce((previous, current) => { - previous[current] = allServices[current] - return previous - }, {}) - - return { - allServices, - services, - children - } -} - -const mapDispatchToProps = dispatch => { - return { - toggleServicesRequest: (name) => { dispatch(PreferencesActions.toggleServicesRequest(name)) }, - completeScreen: (name) => { dispatch(PreferencesActions.completeTourSuccess(name)) } - } -} - -export default connect(mapStateToProps, mapDispatchToProps)(Notifications) diff --git a/App/SB/views/Notifications/index.tsx b/App/SB/views/Notifications/index.tsx new file mode 100644 index 000000000..5617bd8df --- /dev/null +++ b/App/SB/views/Notifications/index.tsx @@ -0,0 +1,247 @@ +import React from 'react' +import { Dispatch } from 'redux' +import { connect } from 'react-redux' +import { View, Text, ScrollView, Platform } from 'react-native' +import { RootAction, RootState } from '../../../Redux/Types' +import PreferencesActions, { ServiceType, TourScreens } from '../../../Redux/PreferencesRedux' +import PermissionsInfo from '../../components/PermissionsInfo' +import HeaderButtons from 'react-navigation-header-buttons' +import SettingsRow from '../../components/SettingsRow' +import GetServiceInfo from './GetServiceInfo' + +import { TextileHeaderButtons, Item as TextileItem } from '../../../Components/HeaderButtons' + +import styles from './statics/styles' +import Avatar from '../../../Components/Avatar' +import { NavigationActions, NavigationScreenProps } from 'react-navigation' + +interface DispatchProps { + toggleServicesRequest: (name: ServiceType) => void + completeScreen: (name: TourScreens) => void + toggleNodeState: () => void + toggleNodeErrors: () => void + toggleNodeStateOverlay: () => void +} + +interface ServiceInfo { + title: string, + subtitle: string, + dependsOn?: string + details?: string +} + +interface ServiceSummary { + [key: string]: {status: boolean, info: ServiceInfo} +} + +interface StateProps { + allServices: ServiceSummary + services: ServiceSummary + children: ServiceSummary + verboseUi: boolean + verboseUiOptions: { + nodeStateOverlay: boolean + nodeStateNotifications: boolean + nodeErrorNotifications: boolean + } +} + +interface OwnProps { + complete: boolean + iOS: boolean + cameraRoll: boolean + locationBackground: boolean + infoVisible: boolean + info?: ServiceInfo +} + +type Props = DispatchProps & StateProps & OwnProps + +class Notifications extends React.PureComponent { + static navigationOptions = ({ navigation }: NavigationScreenProps<{}>) => { + return { + headerTitle: 'Notifications', + headerLeft: ( + + { + navigation.dispatch(NavigationActions.back()) + }} + /> + + ), + headerRight: ( + + } + /> + + ) + } + } + + state: OwnProps = { + complete: false, + iOS: Platform.OS === 'ios', + cameraRoll: false, + locationBackground: false, + infoVisible: false + } + + toggleService = (name: string) => { + if (name === 'notifications') { + // never prompt the user later to get those + this.props.completeScreen(name) + } + this.props.toggleServicesRequest(name as ServiceType) + } + + hideInfo = () => { + this.setState({ infoVisible: false }) + } + + showInfo = (service: string) => { + const info = GetServiceInfo(service) + this.setState({ infoVisible: true, info }) + } + + render () { + return ( + + + Choose the types of notifications you want to receive. + + + + {Object.keys(this.props.services) + .map((service, i) => { + const value = !!this.props.services[service].status + const children = Object.keys(this.props.children) + .filter((key) => this.props.children[key].info.dependsOn === service) + .reduce((previous, current) => { + previous[current] = this.props.children[current] + return previous + }, {}) + + return ( + + + {children && Object.keys(children).map((child, i) => + + )} + + ) + } + )} + + + {this.props.verboseUi && + + + {}} + onChange={this.props.toggleNodeStateOverlay} + /> + + + {}} + onChange={this.props.toggleNodeState} + /> + + + {}} + onChange={this.props.toggleNodeErrors} + /> + + + } + + {this.state.infoVisible && + + } + + ) + } +} + +const mapStateToProps = (state: RootState): StateProps => { + // get all top level services + const allServices = Object.keys(state.preferences.services) + .reduce((previous, current) => { + const basic = { + status: state.preferences.services[current].status, + info: GetServiceInfo(current) + } + previous[current] = basic + return previous + }, {}) + + const services = Object.keys(allServices) + .filter((key) => allServices[key].info !== undefined && allServices[key].info.dependsOn === undefined) + .reduce((previous, current) => { + previous[current] = allServices[current] + return previous + }, {}) + + // get any services that depend on top level services + const children = Object.keys(allServices) + .filter((key) => allServices[key].info !== undefined && allServices[key].info.dependsOn !== undefined) + .reduce((previous, current) => { + previous[current] = allServices[current] + return previous + }, {}) + + return { + allServices, + services, + children, + verboseUi: state.preferences.verboseUi, + verboseUiOptions: state.preferences.verboseUiOptions + } +} + +const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => { + return { + toggleServicesRequest: (name) => { dispatch(PreferencesActions.toggleServicesRequest(name)) }, + completeScreen: (name) => { dispatch(PreferencesActions.completeTourSuccess(name)) }, + toggleNodeState: () => { dispatch(PreferencesActions.toggleNodeStateNotifications()) }, + toggleNodeErrors: () => { dispatch(PreferencesActions.toggleNodeErrorNotifications()) }, + toggleNodeStateOverlay: () => { dispatch(PreferencesActions.toggleNodeStateOverlay()) } + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Notifications) diff --git a/App/Sagas/ImageSharingSagas.ts b/App/Sagas/ImageSharingSagas.ts index 21626b655..72326e798 100644 --- a/App/Sagas/ImageSharingSagas.ts +++ b/App/Sagas/ImageSharingSagas.ts @@ -11,6 +11,7 @@ import AccountActions from '../Redux/AccountRedux' import { groupActions } from '../features/group' import { groupSelectors } from '../features/group' import UIActions, { UISelectors } from '../Redux/UIRedux' +import TextileEventsActions from '../Redux/TextileEventsRedux' import { ActionType, getType } from 'typesafe-actions' import NavigationService from '../Services/NavigationService' import * as CameraRoll from '../Services/CameraRoll' @@ -93,6 +94,7 @@ export function * shareWalletImage (id: string, threadId: string, comment?: stri // TODO: Insert some state into the processing photos redux in case this takes long or fails const blockId: string = yield call(API.files.addByTarget, id, threadId, comment) } catch (error) { + yield put(TextileEventsActions.newErrorMessage('shareWalletImage', error.message)) yield put(UIActions.imageSharingError(error)) } } diff --git a/App/Sagas/NotificationsSagas.ts b/App/Sagas/NotificationsSagas.ts index 311c38099..67b948a6f 100644 --- a/App/Sagas/NotificationsSagas.ts +++ b/App/Sagas/NotificationsSagas.ts @@ -26,16 +26,20 @@ import PhotoViewingAction, { ThreadData } from '../Redux/PhotoViewingRedux' import { threadDataByThreadId } from '../Redux/PhotoViewingSelectors' import { PreferencesSelectors, ServiceType } from '../Redux/PreferencesRedux' import NotificationsActions, { NotificationsSelectors } from '../Redux/NotificationsRedux' +import TextileEventsActions, { TextileEventsSelectors } from '../Redux/TextileEventsRedux' import * as NotificationsServices from '../Services/Notifications' import {logNewEvent} from './DeviceLogs' -import { TextileEventsSelectors } from '../Redux/TextileEventsRedux' export function * enable () { yield call(NotificationsServices.enable) } export function * readAllNotifications (action: ActionType) { - yield call(API.notifications.readAll) + try { + yield call(API.notifications.readAll) + } catch (error) { + yield put(TextileEventsActions.newErrorMessage('readAllNotifications', error.message)) + } } export function * handleNewNotification (action: ActionType) { @@ -142,6 +146,7 @@ export function * refreshNotifications () { const typedNotifs = notificationResponse.items.map((notificationData) => NotificationsServices.toTypedNotification(notificationData)) yield put(NotificationsActions.refreshNotificationsSuccess(typedNotifs)) } catch (error) { + yield put(TextileEventsActions.newErrorMessage('refreshNotifications', error.message)) yield put(NotificationsActions.refreshNotificationsFailure()) } } diff --git a/App/Sagas/PhotoViewingSagas.ts b/App/Sagas/PhotoViewingSagas.ts index 733543367..d473ea93a 100644 --- a/App/Sagas/PhotoViewingSagas.ts +++ b/App/Sagas/PhotoViewingSagas.ts @@ -7,6 +7,7 @@ import PhotoViewingActions from '../Redux/PhotoViewingRedux' import { InboundInvite } from '../Redux/ThreadsRedux' import { inboundInviteByThreadName } from '../Redux/ThreadsSelectors' import { getAddress } from '../Redux/AccountSelectors' +import TextileEventsActions from '../Redux/TextileEventsRedux' import UIActions from '../Redux/UIRedux' import { photoAndComment, shouldNavigateToNewThread, shouldSelectNewThread, photoToShareToNewThread } from '../Redux/PhotoViewingSelectors' import { @@ -53,6 +54,7 @@ export function * monitorThreadAddedNotifications (action: ActionType action.type === getType(TextileEventsActions.newErrorMessage) ) - - if (yield select(PreferencesSelectors.verboseUi)) { + if (yield select(PreferencesSelectors.showNodeErrorNotification)) { yield call(displayNotification, action.payload.type, action.payload.message) } yield call(logNewEvent, action.payload.type, action.payload.message, true)