diff --git a/ui/component/blockButton/index.js b/ui/component/blockButton/index.js index e83040917..33653b277 100644 --- a/ui/component/blockButton/index.js +++ b/ui/component/blockButton/index.js @@ -2,11 +2,11 @@ import { connect } from 'react-redux'; import { selectChannelIsBlocked, doToggleBlockChannel, - doToast, makeSelectClaimIsMine, makeSelectShortUrlForUri, makeSelectPermanentUrlForUri, } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import BlockButton from './view'; const select = (state, props) => ({ @@ -16,10 +16,7 @@ const select = (state, props) => ({ permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state), }); -export default connect( - select, - { - toggleBlockChannel: doToggleBlockChannel, - doToast, - } -)(BlockButton); +export default connect(select, { + toggleBlockChannel: doToggleBlockChannel, + doToast, +})(BlockButton); diff --git a/ui/component/claimUri/index.js b/ui/component/claimUri/index.js index 60e8b720c..622b9048e 100644 --- a/ui/component/claimUri/index.js +++ b/ui/component/claimUri/index.js @@ -1,14 +1,12 @@ import { connect } from 'react-redux'; -import { makeSelectCanonicalUrlForUri, doToast } from 'lbry-redux'; +import { makeSelectCanonicalUrlForUri } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import ClaimUri from './view'; const select = (state, props) => ({ shortUrl: makeSelectCanonicalUrlForUri(props.uri)(state), }); -export default connect( - select, - { - doToast, - } -)(ClaimUri); +export default connect(select, { + doToast, +})(ClaimUri); diff --git a/ui/component/copyableText/index.js b/ui/component/copyableText/index.js index 070ea0e99..c0b2ac443 100644 --- a/ui/component/copyableText/index.js +++ b/ui/component/copyableText/index.js @@ -1,10 +1,7 @@ import { connect } from 'react-redux'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import CopyableText from './view'; -export default connect( - null, - { - doToast, - } -)(CopyableText); +export default connect(null, { + doToast, +})(CopyableText); diff --git a/ui/component/embedTextArea/index.js b/ui/component/embedTextArea/index.js index 78059cb26..8916d1642 100644 --- a/ui/component/embedTextArea/index.js +++ b/ui/component/embedTextArea/index.js @@ -1,10 +1,7 @@ import { connect } from 'react-redux'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import EmbedTextArea from './view'; -export default connect( - null, - { - doToast, - } -)(EmbedTextArea); +export default connect(null, { + doToast, +})(EmbedTextArea); diff --git a/ui/component/publishFile/index.js b/ui/component/publishFile/index.js index bcd1727b6..27bf63322 100644 --- a/ui/component/publishFile/index.js +++ b/ui/component/publishFile/index.js @@ -4,9 +4,9 @@ import { selectIsStillEditing, makeSelectPublishFormValue, doUpdatePublishForm, - doToast, doClearPublish, } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import { selectFfmpegStatus } from 'redux/selectors/settings'; import PublishPage from './view'; diff --git a/ui/component/settingAccountPassword/index.js b/ui/component/settingAccountPassword/index.js index 180f05cde..46e6d59e7 100644 --- a/ui/component/settingAccountPassword/index.js +++ b/ui/component/settingAccountPassword/index.js @@ -6,7 +6,7 @@ import { doUserPasswordSet, doClearPasswordEntry, } from 'lbryinc'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import UserSignIn from './view'; const select = state => ({ diff --git a/ui/component/settingAutoLaunch/index.js b/ui/component/settingAutoLaunch/index.js index 5a08f94f9..b8360f892 100644 --- a/ui/component/settingAutoLaunch/index.js +++ b/ui/component/settingAutoLaunch/index.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux'; import * as SETTINGS from 'constants/settings'; import { doSetAutoLaunch } from 'redux/actions/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import SettingAutoLaunch from './view'; const select = state => ({ @@ -14,7 +14,4 @@ const perform = dispatch => ({ setAutoLaunch: value => dispatch(doSetAutoLaunch(value)), }); -export default connect( - select, - perform -)(SettingAutoLaunch); +export default connect(select, perform)(SettingAutoLaunch); diff --git a/ui/component/shareButton/index.js b/ui/component/shareButton/index.js index 81ed8bf94..a8dd393c4 100644 --- a/ui/component/shareButton/index.js +++ b/ui/component/shareButton/index.js @@ -1,17 +1,14 @@ import { connect } from 'react-redux'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doOpenModal } from 'redux/actions/app'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import ShareButton from './view'; const select = (state, props) => ({}); -export default connect( - select, - { - doChannelSubscribe, - doChannelUnsubscribe, - doOpenModal, - doToast, - } -)(ShareButton); +export default connect(select, { + doChannelSubscribe, + doChannelUnsubscribe, + doOpenModal, + doToast, +})(ShareButton); diff --git a/ui/component/snackBar/index.js b/ui/component/snackBar/index.js index 45bfbade6..9bda252ce 100644 --- a/ui/component/snackBar/index.js +++ b/ui/component/snackBar/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; -import { selectToast, doDismissToast } from 'lbry-redux'; +import { doDismissToast } from 'redux/actions/notifications'; +import { selectToast } from 'redux/selectors/notifications'; import SnackBar from './view'; const perform = dispatch => ({ @@ -10,7 +11,4 @@ const select = state => ({ snack: selectToast(state), }); -export default connect( - select, - perform -)(SnackBar); +export default connect(select, perform)(SnackBar); diff --git a/ui/component/splash/index.js b/ui/component/splash/index.js index 5f8788e00..052e0811d 100644 --- a/ui/component/splash/index.js +++ b/ui/component/splash/index.js @@ -4,7 +4,8 @@ import { selectDaemonVersionMatched, selectModal } from 'redux/selectors/app'; import { doCheckDaemonVersion, doOpenModal, doHideModal } from 'redux/actions/app'; import { doSetClientSetting, doClearDaemonSetting } from 'redux/actions/settings'; import * as settings from 'constants/settings'; -import { doToast, DAEMON_SETTINGS } from 'lbry-redux'; +import { DAEMON_SETTINGS } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import SplashScreen from './view'; import { makeSelectClientSetting } from 'redux/selectors/settings'; @@ -25,7 +26,4 @@ const perform = dispatch => ({ doShowSnackBar: message => dispatch(doToast({ isError: true, message })), }); -export default connect( - select, - perform -)(SplashScreen); +export default connect(select, perform)(SplashScreen); diff --git a/ui/component/subscribeButton/index.js b/ui/component/subscribeButton/index.js index 8186e198b..2a0af6b5b 100644 --- a/ui/component/subscribeButton/index.js +++ b/ui/component/subscribeButton/index.js @@ -2,7 +2,8 @@ import { connect } from 'react-redux'; import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions'; import { doOpenModal } from 'redux/actions/app'; import { selectSubscriptions, makeSelectIsSubscribed, selectFirstRunCompleted } from 'redux/selectors/subscriptions'; -import { doToast, makeSelectPermanentUrlForUri } from 'lbry-redux'; +import { makeSelectPermanentUrlForUri } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import SubscribeButton from './view'; const select = (state, props) => ({ @@ -12,12 +13,9 @@ const select = (state, props) => ({ permanentUrl: makeSelectPermanentUrlForUri(props.uri)(state), }); -export default connect( - select, - { - doChannelSubscribe, - doChannelUnsubscribe, - doOpenModal, - doToast, - } -)(SubscribeButton); +export default connect(select, { + doChannelSubscribe, + doChannelUnsubscribe, + doOpenModal, + doToast, +})(SubscribeButton); diff --git a/ui/component/userEmailVerify/index.js b/ui/component/userEmailVerify/index.js index 1fc6ecfd1..798a250b4 100644 --- a/ui/component/userEmailVerify/index.js +++ b/ui/component/userEmailVerify/index.js @@ -7,7 +7,7 @@ import { selectUser, selectResendingVerificationEmail, } from 'lbryinc'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import UserEmailVerify from './view'; const select = state => ({ diff --git a/ui/component/userPasswordReset/index.js b/ui/component/userPasswordReset/index.js index f6799bd8d..7512c5ce5 100644 --- a/ui/component/userPasswordReset/index.js +++ b/ui/component/userPasswordReset/index.js @@ -8,7 +8,7 @@ import { doClearEmailEntry, selectEmailToVerify, } from 'lbryinc'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import UserSignIn from './view'; const select = state => ({ diff --git a/ui/component/userPasswordSet/index.js b/ui/component/userPasswordSet/index.js index ead9767aa..72841c37e 100644 --- a/ui/component/userPasswordSet/index.js +++ b/ui/component/userPasswordSet/index.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import { doClearEmailEntry, doUserFetch } from 'lbryinc'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import UserSignIn from './view'; const select = state => ({ diff --git a/ui/component/wunderbar/index.js b/ui/component/wunderbar/index.js index 2895ee450..bcad5368c 100644 --- a/ui/component/wunderbar/index.js +++ b/ui/component/wunderbar/index.js @@ -3,11 +3,11 @@ import { doFocusSearchInput, doBlurSearchInput, doUpdateSearchQuery, - doToast, selectSearchValue, selectSearchSuggestions, selectSearchBarFocused, } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import analytics from 'analytics'; import Wunderbar from './view'; import { withRouter } from 'react-router-dom'; @@ -36,9 +36,4 @@ const perform = (dispatch, ownProps) => ({ doBlur: () => dispatch(doBlurSearchInput()), }); -export default withRouter( - connect( - select, - perform - )(Wunderbar) -); +export default withRouter(connect(select, perform)(Wunderbar)); diff --git a/ui/constants/action_types.js b/ui/constants/action_types.js index 7750fa3f5..21fc700f1 100644 --- a/ui/constants/action_types.js +++ b/ui/constants/action_types.js @@ -198,3 +198,13 @@ export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT'; // media export const MEDIA_PLAY = 'MEDIA_PLAY'; export const MEDIA_PAUSE = 'MEDIA_PAUSE'; + +// Notifications +export const CREATE_NOTIFICATION = 'CREATE_NOTIFICATION'; +export const EDIT_NOTIFICATION = 'EDIT_NOTIFICATION'; +export const DELETE_NOTIFICATION = 'DELETE_NOTIFICATION'; +export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION'; +export const CREATE_TOAST = 'CREATE_TOAST'; +export const DISMISS_TOAST = 'DISMISS_TOAST'; +export const CREATE_ERROR = 'CREATE_ERROR'; +export const DISMISS_ERROR = 'DISMISS_ERROR'; diff --git a/ui/index.jsx b/ui/index.jsx index 96ef3c583..982bb2804 100644 --- a/ui/index.jsx +++ b/ui/index.jsx @@ -14,7 +14,7 @@ import React, { Fragment, useState, useEffect } from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { doDaemonReady, doAutoUpdate, doOpenModal, doHideModal, doToggle3PAnalytics } from 'redux/actions/app'; -import { Lbry, doToast, isURIValid, setSearchApi, apiCall } from 'lbry-redux'; +import { Lbry, isURIValid, setSearchApi, apiCall } from 'lbry-redux'; import { doSetLanguage, doFetchLanguage, doUpdateIsNightAsync } from 'redux/actions/settings'; import { Lbryio, rewards, doBlackListedOutpointsSubscribe, doFilteredOutpointsSubscribe } from 'lbryinc'; import { store, persistor, history } from 'store'; @@ -24,6 +24,7 @@ import { ConnectedRouter, push } from 'connected-react-router'; import { formatLbryUrlForWeb, formatInAppUrl } from 'util/url'; import { PersistGate } from 'redux-persist/integration/react'; import analytics from 'analytics'; +import { doToast } from 'redux/actions/notifications'; import { getAuthToken, setAuthToken, diff --git a/ui/modal/modalAutoGenerateThumbnail/index.js b/ui/modal/modalAutoGenerateThumbnail/index.js index 727140e3b..d769e869b 100644 --- a/ui/modal/modalAutoGenerateThumbnail/index.js +++ b/ui/modal/modalAutoGenerateThumbnail/index.js @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; import { doHideModal } from 'redux/actions/app'; -import { doToast, doUploadThumbnail } from 'lbry-redux'; +import { doUploadThumbnail } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import ModalAutoGenerateThumbnail from './view'; const perform = dispatch => ({ @@ -9,7 +10,4 @@ const perform = dispatch => ({ showToast: options => dispatch(doToast(options)), }); -export default connect( - null, - perform -)(ModalAutoGenerateThumbnail); +export default connect(null, perform)(ModalAutoGenerateThumbnail); diff --git a/ui/modal/modalError/index.js b/ui/modal/modalError/index.js index f0a590f35..484a81147 100644 --- a/ui/modal/modalError/index.js +++ b/ui/modal/modalError/index.js @@ -1,12 +1,9 @@ import { connect } from 'react-redux'; -import { doDismissError } from 'lbry-redux'; +import { doDismissError } from 'redux/actions/notifications'; import ModalError from './view'; const perform = dispatch => ({ closeModal: () => dispatch(doDismissError()), }); -export default connect( - null, - perform -)(ModalError); +export default connect(null, perform)(ModalError); diff --git a/ui/modal/modalRepost/index.js b/ui/modal/modalRepost/index.js index a5c76eb69..054a266ed 100644 --- a/ui/modal/modalRepost/index.js +++ b/ui/modal/modalRepost/index.js @@ -9,10 +9,10 @@ import { selectRepostError, selectRepostLoading, doClearRepostError, - doToast, selectMyClaimsWithoutChannels, doCheckPublishNameAvailability, } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import ModalRepost from './view'; const select = (state, props) => ({ diff --git a/ui/page/report/view.jsx b/ui/page/report/view.jsx index 2dd124505..53f219d4f 100644 --- a/ui/page/report/view.jsx +++ b/ui/page/report/view.jsx @@ -1,7 +1,7 @@ import React, { Fragment } from 'react'; import Button from 'component/button'; import { FormField } from 'component/common/form'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import { Lbryio } from 'lbryinc'; import Page from 'component/page'; import Card from 'component/common/card'; diff --git a/ui/page/signInVerify/index.js b/ui/page/signInVerify/index.js index c00a9b77c..7c8a3aae9 100644 --- a/ui/page/signInVerify/index.js +++ b/ui/page/signInVerify/index.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import { doToast } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import SignInVerifyPage from './view'; const select = () => ({}); @@ -7,7 +7,4 @@ const perform = { doToast, }; -export default connect( - select, - perform -)(SignInVerifyPage); +export default connect(select, perform)(SignInVerifyPage); diff --git a/ui/redux/actions/app.js b/ui/redux/actions/app.js index d603fab48..6ed23cc38 100644 --- a/ui/redux/actions/app.js +++ b/ui/redux/actions/app.js @@ -11,18 +11,17 @@ import { Lbry, doBalanceSubscribe, doFetchFileInfos, - doError, makeSelectClaimForUri, makeSelectClaimIsMine, doPopulateSharedUserState, doFetchChannelListMine, doClearPublish, doPreferenceGet, - doToast, doClearSupport, selectFollowedTagsList, // SHARED_PREFERENCES, } from 'lbry-redux'; +import { doToast, doError } from 'redux/actions/notifications'; import Native from 'native'; import { doFetchDaemonSettings, diff --git a/ui/redux/actions/notifications.js b/ui/redux/actions/notifications.js new file mode 100644 index 000000000..78d0ac90b --- /dev/null +++ b/ui/redux/actions/notifications.js @@ -0,0 +1,38 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import uuid from 'uuid/v4'; + +export function doToast(params: ToastParams) { + if (!params) { + throw Error("'params' object is required to create a toast notification"); + } + + return { + type: ACTIONS.CREATE_TOAST, + data: { + id: uuid(), + params, + }, + }; +} + +export function doDismissToast() { + return { + type: ACTIONS.DISMISS_TOAST, + }; +} + +export function doError(error: string | {}) { + return { + type: ACTIONS.CREATE_ERROR, + data: { + error, + }, + }; +} + +export function doDismissError() { + return { + type: ACTIONS.DISMISS_ERROR, + }; +} diff --git a/ui/redux/actions/publish.js b/ui/redux/actions/publish.js index b5528edb5..32158b5cc 100644 --- a/ui/redux/actions/publish.js +++ b/ui/redux/actions/publish.js @@ -4,13 +4,13 @@ import * as ACTIONS from 'constants/action_types'; import * as PAGES from 'constants/pages'; import { batchActions, - doError, selectMyClaims, doPublish, doCheckPendingPublishes, doCheckReflectingFiles, ACTIONS as LBRY_REDUX_ACTIONS, } from 'lbry-redux'; +import { doError } from 'redux/actions/notifications'; import { selectosNotificationsEnabled } from 'redux/selectors/settings'; import { push } from 'connected-react-router'; import analytics from 'analytics'; diff --git a/ui/redux/actions/settings.js b/ui/redux/actions/settings.js index 4caca0163..78931d0f4 100644 --- a/ui/redux/actions/settings.js +++ b/ui/redux/actions/settings.js @@ -1,4 +1,5 @@ -import { Lbry, ACTIONS, doToast, SHARED_PREFERENCES, doWalletReconnect, SETTINGS } from 'lbry-redux'; +import { Lbry, ACTIONS, SHARED_PREFERENCES, doWalletReconnect, SETTINGS } from 'lbry-redux'; +import { doToast } from 'redux/actions/notifications'; import * as LOCAL_ACTIONS from 'constants/action_types'; import analytics from 'analytics'; import SUPPORTED_LANGUAGES from 'constants/supported_languages'; diff --git a/ui/redux/reducers/notifications.js b/ui/redux/reducers/notifications.js new file mode 100644 index 000000000..d84c78edc --- /dev/null +++ b/ui/redux/reducers/notifications.js @@ -0,0 +1,94 @@ +// @flow +import * as ACTIONS from 'constants/action_types'; +import { handleActions } from 'util/redux-utils'; + +const defaultState: NotificationState = { + notifications: [], + toasts: [], + errors: [], +}; + +const notificationsReducer = handleActions( + { + // Toasts + [ACTIONS.CREATE_TOAST]: (state: NotificationState, action: DoToast) => { + const toast: Toast = action.data; + const newToasts: Array = state.toasts.slice(); + newToasts.push(toast); + + return { + ...state, + toasts: newToasts, + }; + }, + [ACTIONS.DISMISS_TOAST]: (state: NotificationState) => { + const newToasts: Array = state.toasts.slice(); + newToasts.shift(); + + return { + ...state, + toasts: newToasts, + }; + }, + + // Notifications + [ACTIONS.CREATE_NOTIFICATION]: (state: NotificationState, action: DoNotification) => { + const notification: Notification = action.data; + const newNotifications: Array = state.notifications.slice(); + newNotifications.push(notification); + + return { + ...state, + notifications: newNotifications, + }; + }, + // Used to mark notifications as read/dismissed + [ACTIONS.EDIT_NOTIFICATION]: (state: NotificationState, action: DoEditNotification) => { + const { notification } = action.data; + let notifications: Array = state.notifications.slice(); + + notifications = notifications.map(pastNotification => + pastNotification.id === notification.id ? notification : pastNotification + ); + + return { + ...state, + notifications, + }; + }, + [ACTIONS.DELETE_NOTIFICATION]: (state: NotificationState, action: DoDeleteNotification) => { + const { id } = action.data; + let newNotifications: Array = state.notifications.slice(); + newNotifications = newNotifications.filter(notification => notification.id !== id); + + return { + ...state, + notifications: newNotifications, + }; + }, + + // Errors + [ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoError) => { + const error: ErrorNotification = action.data; + const newErrors: Array = state.errors.slice(); + newErrors.push(error); + + return { + ...state, + errors: newErrors, + }; + }, + [ACTIONS.DISMISS_ERROR]: (state: NotificationState) => { + const newErrors: Array = state.errors.slice(); + newErrors.shift(); + + return { + ...state, + errors: newErrors, + }; + }, + }, + defaultState +); + +export { notificationsReducer }; diff --git a/ui/redux/selectors/notifications.js b/ui/redux/selectors/notifications.js new file mode 100644 index 000000000..8de3a1488 --- /dev/null +++ b/ui/redux/selectors/notifications.js @@ -0,0 +1,26 @@ +import { createSelector } from 'reselect'; + +export const selectState = state => state.notifications || {}; + +export const selectToast = createSelector(selectState, state => { + if (state.toasts.length) { + const { id, params } = state.toasts[0]; + return { + id, + ...params, + }; + } + + return null; +}); + +export const selectError = createSelector(selectState, state => { + if (state.errors.length) { + const { error } = state.errors[0]; + return { + error, + }; + } + + return null; +});