use new notification state

This commit is contained in:
Sean Yesmunt 2018-11-12 13:01:14 -05:00
parent 6139cede26
commit b295a7db57
12 changed files with 1041 additions and 772 deletions

1368
dist/bundle.js vendored

File diff suppressed because it is too large Load diff

View file

@ -28,7 +28,8 @@
}, },
"dependencies": { "dependencies": {
"proxy-polyfill": "0.1.6", "proxy-polyfill": "0.1.6",
"reselect": "^3.0.0" "reselect": "^3.0.0",
"uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.0",

View file

@ -210,5 +210,10 @@ export const REMOVE_PENDING_PUBLISH = 'REMOVE_PENDING_PUBLISH';
export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT'; export const DO_PREPARE_EDIT = 'DO_PREPARE_EDIT';
// Notifications // Notifications
export const CREATE_NOTIFICATION = 'CREATE_NOTIFICATION'; export const CREATE_EVENT = 'CREATE_EVENT';
export const DISMISS_NOTIFICATION = 'DISMISS_NOTIFICATION'; export const EDIT_EVENT = 'EDIT_EVENT';
export const DELETE_EVENT = 'DELETE_EVENT';
export const CREATE_TOAST = 'CREATE_TOAST';
export const DISMISS_TOAST = 'DISMISS_TOAST';
export const CREATE_ERROR = 'CREATE_ERROR';
export const DISMISS_ERROR = 'DISMISS_ERROR';

View file

@ -1,30 +0,0 @@
export const CONFIRM_FILE_REMOVE = 'confirm_file_remove';
export const CONFIRM_EXTERNAL_LINK = 'confirm_external_link';
export const INCOMPATIBLE_DAEMON = 'incompatible_daemon';
export const FILE_TIMEOUT = 'file_timeout';
export const DOWNLOADING = 'downloading';
export const AUTO_UPDATE_DOWNLOADED = 'auto_update_downloaded';
export const AUTO_UPDATE_CONFIRM = 'auto_update_confirm';
export const ERROR = 'error';
export const INSUFFICIENT_CREDITS = 'insufficient_credits';
export const UPGRADE = 'upgrade';
export const WELCOME = 'welcome';
export const EMAIL_COLLECTION = 'email_collection';
export const PHONE_COLLECTION = 'phone_collection';
export const FIRST_REWARD = 'first_reward';
export const AUTHENTICATION_FAILURE = 'auth_failure';
export const TRANSACTION_FAILED = 'transaction_failed';
export const REWARD_APPROVAL_REQUIRED = 'reward_approval_required';
export const REWARD_GENERATED_CODE = 'reward_generated_code';
export const AFFIRM_PURCHASE = 'affirm_purchase';
export const CONFIRM_CLAIM_REVOKE = 'confirm_claim_revoke';
export const FIRST_SUBSCRIPTION = 'firstSubscription';
export const SEND_TIP = 'send_tip';
export const SOCIAL_SHARE = 'social_share';
export const PUBLISH = 'publish';
export const SEARCH = 'search';
export const CONFIRM_TRANSACTION = 'confirm_transaction';
export const CONFIRM_THUMBNAIL_UPLOAD = 'confirm_thumbnail_upload';
export const WALLET_ENCRYPT = 'wallet_encrypt';
export const WALLET_DECRYPT = 'wallet_decrypt';
export const WALLET_UNLOCK = 'wallet_unlock';

View file

@ -1,5 +1,4 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types';
import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses'; import * as THUMBNAIL_STATUSES from 'constants/thumbnail_upload_statuses';
import * as SEARCH_TYPES from 'constants/search'; import * as SEARCH_TYPES from 'constants/search';
import * as SETTINGS from 'constants/settings'; import * as SETTINGS from 'constants/settings';
@ -12,19 +11,10 @@ import Lbryapi from 'lbryapi';
import { selectState as selectSearchState } from 'redux/selectors/search'; import { selectState as selectSearchState } from 'redux/selectors/search';
// types // types
export { Notification } from 'types/Notification'; export { Toast } from 'types/Notification';
// constants // constants
export { export { ACTIONS, THUMBNAIL_STATUSES, SEARCH_TYPES, SETTINGS, TRANSACTIONS, SORT_OPTIONS, PAGES };
ACTIONS,
MODALS,
THUMBNAIL_STATUSES,
SEARCH_TYPES,
SETTINGS,
TRANSACTIONS,
SORT_OPTIONS,
PAGES,
};
// common // common
export { Lbry, Lbryapi }; export { Lbry, Lbryapi };
@ -41,7 +31,7 @@ export {
} from 'lbryURI'; } from 'lbryURI';
// actions // actions
export { doNotify, doHideNotification } from 'redux/actions/notifications'; export { doToast, doDismissToast, doError, doDismissError } from 'redux/actions/notifications';
export { export {
doFetchClaimsByChannel, doFetchClaimsByChannel,
@ -108,11 +98,7 @@ export { blacklistReducer } from 'redux/reducers/blacklist';
// selectors // selectors
export { selectBlackListedOutpoints } from 'redux/selectors/blacklist'; export { selectBlackListedOutpoints } from 'redux/selectors/blacklist';
export { export { selectToast, selectError } from 'redux/selectors/notifications';
selectNotification,
selectNotificationProps,
selectSnack,
} from 'redux/selectors/notifications';
export { export {
makeSelectClaimForUri, makeSelectClaimForUri,

View file

@ -2,7 +2,7 @@ import * as ACTIONS from 'constants/action_types';
import Lbry from 'lbry'; import Lbry from 'lbry';
import Lbryapi from 'lbryapi'; import Lbryapi from 'lbryapi';
import { normalizeURI } from 'lbryURI'; import { normalizeURI } from 'lbryURI';
import { doNotify } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectMyClaimsRaw, selectResolvingUris } from 'redux/selectors/claims'; import { selectMyClaimsRaw, selectResolvingUris } from 'redux/selectors/claims';
import { batchActions } from 'util/batchActions'; import { batchActions } from 'util/batchActions';
import { doFetchTransactions } from 'redux/actions/wallet'; import { doFetchTransactions } from 'redux/actions/wallet';
@ -86,11 +86,9 @@ export function doAbandonClaim(txid, nout) {
const errorCallback = () => { const errorCallback = () => {
dispatch( dispatch(
doNotify({ doToast({
title: 'Transaction failed', message: 'Transaction failed',
message: 'Error abandoning claim', isError: true,
type: 'error',
displayType: ['snackbar', 'toast'],
}) })
); );
}; };
@ -104,10 +102,8 @@ export function doAbandonClaim(txid, nout) {
}, },
}); });
dispatch( dispatch(
doNotify({ doToast({
title: 'Transaction successful',
message: 'Successfully abandoned your claim', message: 'Successfully abandoned your claim',
displayType: ['snackbar', 'toast'],
}) })
); );
@ -117,11 +113,9 @@ export function doAbandonClaim(txid, nout) {
dispatch(doFetchTransactions()); dispatch(doFetchTransactions());
} else { } else {
dispatch( dispatch(
doNotify({ doToast({
title: 'Transaction failed',
message: 'Error abandoning claim', message: 'Error abandoning claim',
type: 'error', isError: true,
displayType: ['snackbar', 'toast'],
}) })
); );
} }

View file

@ -1,20 +1,39 @@
// @flow // @flow
import type { ToastParams } from 'types/Notification';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import type { Notification, NotificationProps } from 'types/Notification'; import uuid from 'uuid/v4';
export function doToast(params: ToastParams) {
if (!params) {
throw Error("'params' object is required to create a toast notification");
}
export function doNotify(notification: Notification, notificationProps: NotificationProps) {
return { return {
type: ACTIONS.CREATE_NOTIFICATION, type: ACTIONS.CREATE_TOAST,
data: { data: {
notification, id: uuid(),
// using this syntax to create an object if notificationProps is undefined params,
notificationProps: { ...notificationProps },
}, },
}; };
} }
export function doHideNotification() { export function doDismissToast() {
return { return {
type: ACTIONS.DISMISS_NOTIFICATION, type: ACTIONS.DISMISS_TOAST,
};
}
export function doError(error: string | {}) {
return {
type: ACTIONS.CREATE_ERROR,
data: {
error,
},
};
}
export function doDismissError() {
return {
type: ACTIONS.DISMISS_ERROR,
}; };
} }

View file

@ -1,6 +1,6 @@
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import Lbry from 'lbry'; import Lbry from 'lbry';
import { doNotify } from 'redux/actions/notifications'; import { doToast } from 'redux/actions/notifications';
import { selectBalance } from 'redux/selectors/wallet'; import { selectBalance } from 'redux/selectors/wallet';
import { creditsToString } from 'util/formatCredits'; import { creditsToString } from 'util/formatCredits';
@ -96,11 +96,9 @@ export function doSendDraftTransaction(address, amount) {
if (balance - amount <= 0) { if (balance - amount <= 0) {
dispatch( dispatch(
doNotify({ doToast({
title: 'Insufficient credits', title: 'Insufficient credits',
message: 'Insufficient credits', message: 'Insufficient credits',
type: 'error',
displayType: ['modal', 'toast'],
}) })
); );
return; return;
@ -116,11 +114,8 @@ export function doSendDraftTransaction(address, amount) {
type: ACTIONS.SEND_TRANSACTION_COMPLETED, type: ACTIONS.SEND_TRANSACTION_COMPLETED,
}); });
dispatch( dispatch(
doNotify({ doToast({
title: 'Credits sent',
message: `You sent ${amount} LBC`, message: `You sent ${amount} LBC`,
type: 'error',
displayType: ['snackbar', 'toast'],
linkText: 'History', linkText: 'History',
linkTarget: '/wallet', linkTarget: '/wallet',
}) })
@ -131,11 +126,9 @@ export function doSendDraftTransaction(address, amount) {
data: { error: response }, data: { error: response },
}); });
dispatch( dispatch(
doNotify({ doToast({
title: 'Transaction failed',
message: 'Transaction failed', message: 'Transaction failed',
type: 'error', isError: true,
displayType: ['snackbar', 'toast'],
}) })
); );
} }
@ -147,11 +140,9 @@ export function doSendDraftTransaction(address, amount) {
data: { error: error.message }, data: { error: error.message },
}); });
dispatch( dispatch(
doNotify({ doToast({
title: 'Transaction failed',
message: 'Transaction failed', message: 'Transaction failed',
type: 'error', isError: true,
displayType: ['snackbar', 'toast'],
}) })
); );
}; };
@ -184,11 +175,9 @@ export function doSendTip(amount, claimId, uri, successCallback, errorCallback)
if (balance - amount <= 0) { if (balance - amount <= 0) {
dispatch( dispatch(
doNotify({ doToast({
title: 'Insufficient credits',
message: 'Insufficient credits', message: 'Insufficient credits',
type: 'error', isError: true,
displayType: ['modal', 'toast'],
}) })
); );
return; return;
@ -196,11 +185,10 @@ export function doSendTip(amount, claimId, uri, successCallback, errorCallback)
const success = () => { const success = () => {
dispatch( dispatch(
doNotify({ doToast({
message: __(`You sent ${amount} LBC as a tip, Mahalo!`), message: __(`You sent ${amount} LBC as a tip, Mahalo!`),
linkText: __('History'), linkText: __('History'),
linkTarget: __('/wallet'), linkTarget: __('/wallet'),
displayType: ['snackbar'],
}) })
); );
@ -215,9 +203,9 @@ export function doSendTip(amount, claimId, uri, successCallback, errorCallback)
const error = (err) => { const error = (err) => {
dispatch( dispatch(
doNotify({ doToast({
message: __(`There was an error sending support funds.`), message: __(`There was an error sending support funds.`),
displayType: ['snackbar'], isError: true,
}) })
); );

View file

@ -1,55 +1,99 @@
// @flow
import type {
NotificationState,
DoToast,
DoEvent,
DoEditEvent,
DoDeleteEvent,
} from 'types/Notification';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types'; import { handleActions } from 'util/redux-utils';
const reducers = {}; const defaultState: NotificationState = {
events: [],
const defaultState = { toasts: [],
// First-in, first-out errors: [],
queue: [],
}; };
reducers[ACTIONS.CREATE_NOTIFICATION] = (state, action) => { const notificationsReducer = handleActions(
const { notification, notificationProps } = action.data; {
const { title, message, type, error, displayType, id } = notification; // Toasts
[ACTIONS.CREATE_TOAST]: (state: NotificationState, action: DoToast) => {
const toast = action.data;
const newToasts = state.toasts.slice();
newToasts.push(toast);
const queue = Object.assign([], state.queue); return {
queue.push({ ...state,
notification: { toasts: newToasts,
id, };
title,
message,
type,
error,
displayType,
}, },
notificationProps, [ACTIONS.DISMISS_TOAST]: (state: NotificationState) => {
}); const newToasts = state.toasts.slice();
newToasts.shift();
return Object.assign({}, state, { return {
queue, ...state,
}); toasts: newToasts,
}; };
},
reducers[ACTIONS.DISMISS_NOTIFICATION] = state => { // Events
const queue = Object.assign([], state.queue); [ACTIONS.CREATE_EVENT]: (state: NotificationState, action: DoEvent) => {
queue.shift(); const event = action.data;
const newEvents = state.events.slice();
newEvents.push(event);
return Object.assign({}, state, { return {
queue, ...state,
}); events: newEvents,
}; };
},
// Used to mark notifications as read/dismissed
[ACTIONS.EDIT_EVENT]: (state: NotificationState, action: DoEditEvent) => {
const { event } = action.data;
let events = state.events.slice();
reducers[ACTIONS.HISTORY_NAVIGATE] = state => { events = events.map((pastEvent) => (pastEvent.id === event.id ? event : pastEvent));
const queue = Object.assign([], state.queue);
if (queue[0] && queue[0].notification.id === MODALS.SEARCH) {
queue.shift();
return Object.assign({}, state, { queue });
}
return state;
};
export function notificationsReducer(state = defaultState, action) { return {
const handler = reducers[action.type]; ...state,
if (handler) return handler(state, action); events,
return state; };
} },
[ACTIONS.DELETE_EVENT]: (state: NotificationState, action: DoDeleteEvent) => {
const { id } = action.data;
let newEvents = state.events.slice();
newEvents = newEvents.filter((notification) => notification.id !== id);
return {
...state,
events: newEvents,
};
},
// Errors
[ACTIONS.CREATE_ERROR]: (state: NotificationState, action: DoToast) => {
const error = action.data;
const newErrors = state.errors.slice();
newErrors.push(error);
return {
...state,
errors: newErrors,
};
},
[ACTIONS.DISMISS_ERROR]: (state: NotificationState) => {
const newErrors = state.errors.slice();
newErrors.shift();
return {
...state,
errors: newErrors,
};
},
},
defaultState
);
export { notificationsReducer };

View file

@ -1,30 +1,26 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
export const selectState = state => state.notifications || {}; export const selectState = (state) => state.notifications || {};
export const selectNotificationData = createSelector( export const selectToast = createSelector(selectState, (state) => {
selectState, if (state.toasts.length) {
state => (state.queue.length > 0 ? state.queue[0] : {}) const { id, params } = state.toasts[0];
); return {
id,
export const selectNotification = createSelector( ...params,
selectNotificationData, };
notificationData => notificationData.notification
);
export const selectNotificationProps = createSelector(
selectNotificationData,
notificationData => notificationData.notificationProps
);
export const selectSnack = createSelector(
// No props for snackbar
selectNotification,
notification => {
if (notification && notification.displayType) {
return notification.displayType.indexOf('snackbar') > -1 ? notification : undefined;
}
return undefined;
} }
);
return null;
});
export const selectError = createSelector(selectState, (state) => {
if (state.errors.length) {
const { error } = state.errors[0];
return {
error,
};
}
return null;
});

View file

@ -1,19 +1,97 @@
// @flow // @flow
export type Notification = { import * as ACTIONS from 'constants/action_types';
id: ?string,
title: ?string, /*
Toasts:
- First-in, first-out queue
- Simple messages that are shown in response to user interactions
- Never saved
- If they are the result of errors, use the isError flag when creating
- For errors that should interrupt user behavior, use Error
*/
export type ToastParams = {
message: string, message: string,
type: string, title?: string,
error: ?string, linkText?: string,
displayType: mixed, linkTarget?: string,
isError?: boolean,
// additional properties for SnackBar
linkText: ?string,
linkTarget: ?string,
}; };
// Used for retreiving data from redux store export type Toast = {
export type NotificationProps = { id: string,
uri: ?string, params: ToastParams,
path: ?string, };
export type DoToast = {
type: ACTIONS.CREATE_TOAST,
data: Toast,
};
/*
Events:
- List of notifications based on user interactions/app events
- Always saved, but can be manually deleted
- Can happen in the background, or because of user interaction (ex: publish confirmed)
*/
export type Event = {
id: string, // Unique id
dateCreated: number,
isRead: boolean, // Used to display "new" notifications that a user hasn't seen yet
source?: string, // The type/area an event is from. Used for sorting (ex: publishes, transactions)
// We may want to use priority/isDismissed in the future to specify how urgent a notification is
// and if the user should see it immediately
// isDissmied: boolean,
// priority?: number
};
export type DoEvent = {
type: ACTIONS.CREATE_EVENT,
data: Event,
};
export type DoEditEvent = {
type: ACTIONS.EDIT_EVENT,
data: {
id: string,
isRead: boolean,
// In the future we can add `isDismissed` if we decide to show notifications as they come in
// Similar to Facebook's notifications in the corner of the screen
// isDismissed: boolean,
},
};
export type DoDeleteEvent = {
type: ACTIONS.DELETE_EVENT,
data: {
id: string, // The id to delete
},
};
/*
Errors:
- First-in, first-out queue
- Errors that should interupt user behavior
- For errors that can be shown without interrupting a user, use Toast with the isError flag
*/
export type Error = {
title: string,
text: string,
};
export type DoError = {
type: ACTIONS.CREATE_ERROR,
data: Error,
};
export type DoDismissError = {
type: ACTIONS.DISMISS_ERROR,
};
/*
NotificationState
*/
export type NotificationState = {
events: Array<Event>,
errors: Array<Error>,
toasts: Array<Toast>,
}; };

View file

@ -5977,6 +5977,10 @@ uuid@^3.1.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
v8-compile-cache@^1.1.2: v8-compile-cache@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-1.1.2.tgz#8d32e4f16974654657e676e0e467a348e89b0dc4"