diff --git a/.gitignore b/.gitignore index 27fb5f91..b384ed9e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ __coverage__ .build-info .sass-cache -dist +#dist node_modules _auto_doc_ .vscode diff --git a/__tests__/__snapshots__/index.js.snap b/__tests__/__snapshots__/index.js.snap index 31e05077..33482195 100644 --- a/__tests__/__snapshots__/index.js.snap +++ b/__tests__/__snapshots__/index.js.snap @@ -106,6 +106,18 @@ Object { "getUserSrmDone": [Function], "getUserSrmInit": [Function], }, + "notifications": Object { + "dismissChallengeNotificationsDone": [Function], + "dismissChallengeNotificationsInit": [Function], + "getNotificationsDone": [Function], + "getNotificationsInit": [Function], + "markAllNotificationAsReadDone": [Function], + "markAllNotificationAsReadInit": [Function], + "markAllNotificationAsSeenDone": [Function], + "markAllNotificationAsSeenInit": [Function], + "markNotificationAsReadDone": [Function], + "markNotificationAsReadInit": [Function], + }, "profile": Object { "addSkillDone": [Function], "addSkillInit": [Function], @@ -255,6 +267,7 @@ Object { "memberTasks": [Function], "members": [Function], "mySubmissionsManagement": [Function], + "notifications": [Function], "profile": [Function], "reviewOpportunity": [Function], "settings": [Function], @@ -312,6 +325,10 @@ Object { "default": undefined, "getService": [Function], }, + "notifications": Object { + "default": undefined, + "getService": [Function], + }, "reviewOpportunities": Object { "default": undefined, "getReviewOpportunitiesService": [Function], diff --git a/src/actions/index.js b/src/actions/index.js index 09deaca7..b053ae50 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -14,6 +14,7 @@ import lookupActions from './lookup'; import settingsActions from './settings'; import lookerActions from './looker'; import memberSearchActions from './member-search'; +import notificationActions from './notifications'; export const actions = { auth: authActions.auth, @@ -32,6 +33,7 @@ export const actions = { settings: settingsActions.settings, looker: lookerActions.looker, memberSearch: memberSearchActions.memberSearch, + notifications: notificationActions.notifications, }; export default undefined; diff --git a/src/actions/notifications.js b/src/actions/notifications.js new file mode 100644 index 00000000..d7730ef3 --- /dev/null +++ b/src/actions/notifications.js @@ -0,0 +1,173 @@ +/** + * @module "actions.notifications" + * @desc Actions related to notifications data. + */ + +import _ from 'lodash'; +import { createActions } from 'redux-actions'; +import { getService } from '../services/notifications'; + +/** + * TODO: We will need to change this based on API and + * frontend mapping we need later. + */ +function processData(data) { + const retData = _.map(data, (item) => { + const object = {}; + object.id = item.id; + object.sourceId = item.contents.id; + object.sourceName = item.contents.name || item.contents.title; + object.eventType = item.type; + object.isRead = item.read; + object.isSeen = item.seen; + object.contents = item.contents.message || item.contents.title; + object.version = item.version; + object.date = item.createdAt; + return object; + }); + return retData; +} + +/** + * @static + * @desc Creates an action that signals beginning of notifications + * loading. + * @return {Action} + */ +function getNotificationsInit() { + return { }; +} + +/** + * @static + * @desc Creates an action that loads member achievements. + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function getNotificationsDone(tokenV3) { + let data; + try { + data = await getService(tokenV3).getNotifications(); + } catch (e) { + data = []; + } + return processData(data.items || []); +} + +/** + * @static + * @desc Creates an action that signals beginning of mark notification as read + * loading. + * @return {Action} + */ +function markNotificationAsReadInit() { + return { }; +} + +/** + * @static + * @desc Creates an action that marks notification as read. + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function markNotificationAsReadDone(item, tokenV3) { + try { + await getService(tokenV3).markNotificationAsRead(item.id); + } catch (e) { + return e; + } + return item; +} + +/** + * @static + * @desc Creates an action that signals beginning of mark all notification as read + * loading. + * @return {Action} + */ +function markAllNotificationAsReadInit() { + return { }; +} + +/** + * @static + * @desc Creates an action that marks all notification as read. + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function markAllNotificationAsReadDone(tokenV3) { + try { + await getService(tokenV3).markAllNotificationAsRead(); + } catch (e) { + return e; + } + return true; +} + + +/** + * @static + * @desc Creates an action that signals beginning of mark all notification as seen + * loading. + * @return {Action} + */ +function markAllNotificationAsSeenInit() { + return { }; +} + +/** + * @static + * @desc Creates an action that marks all notification as seen. + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function markAllNotificationAsSeenDone(items, tokenV3) { + try { + await getService(tokenV3).markAllNotificationAsSeen(items); + } catch (e) { + return e; + } + return items; +} + + +/** + * @static + * @desc Creates an action that signals beginning of dismiss all challenge notifications + * loading. + * @return {Action} + */ +function dismissChallengeNotificationsInit() { + return { }; +} + +/** + * @static + * @desc Creates an action that dismisses all challenge notifications + * @param {String} tokenV3 v3 auth token. + * @return {Action} + */ +async function dismissChallengeNotificationsDone(challengeId, tokenV3) { + try { + await getService(tokenV3).dismissChallengeNotifications(challengeId); + } catch (e) { + return e; + } + return true; +} + + +export default createActions({ + NOTIFICATIONS: { + GET_NOTIFICATIONS_INIT: getNotificationsInit, + GET_NOTIFICATIONS_DONE: getNotificationsDone, + MARK_NOTIFICATION_AS_READ_INIT: markNotificationAsReadInit, + MARK_NOTIFICATION_AS_READ_DONE: markNotificationAsReadDone, + MARK_ALL_NOTIFICATION_AS_READ_INIT: markAllNotificationAsReadInit, + MARK_ALL_NOTIFICATION_AS_READ_DONE: markAllNotificationAsReadDone, + MARK_ALL_NOTIFICATION_AS_SEEN_INIT: markAllNotificationAsSeenInit, + MARK_ALL_NOTIFICATION_AS_SEEN_DONE: markAllNotificationAsSeenDone, + DISMISS_CHALLENGE_NOTIFICATIONS_INIT: dismissChallengeNotificationsInit, + DISMISS_CHALLENGE_NOTIFICATIONS_DONE: dismissChallengeNotificationsDone, + }, +}); diff --git a/src/reducers/index.js b/src/reducers/index.js index 7f7e3604..e016d5b0 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -12,6 +12,7 @@ import errors, { factory as errorsFactory } from './errors'; import challenge, { factory as challengeFactory } from './challenge'; import profile, { factory as profileFactory } from './profile'; import members, { factory as membersFactory } from './members'; +import notifications, { factory as notificationsFactory } from './notifications'; import lookup, { factory as lookupFactory } from './lookup'; import memberTasks, { factory as memberTasksFactory } from './member-tasks'; import reviewOpportunity, { factory as reviewOpportunityFactory } @@ -42,6 +43,7 @@ export function factory(options) { settings: settingsFactory(options), looker: lookerFactory(options), memberSearch: memberSearchFactory(options), + notifications: notificationsFactory(options), }); } @@ -62,4 +64,5 @@ export default ({ settings, looker, memberSearch, + notifications, }); diff --git a/src/reducers/notifications.js b/src/reducers/notifications.js new file mode 100644 index 00000000..bb1ecaa8 --- /dev/null +++ b/src/reducers/notifications.js @@ -0,0 +1,254 @@ +/** + * @module "reducers.notifications" + * @desc Reducer for {@link module:actions.notifications} actions. + * + * State segment managed by this reducer has the following strcuture: + * @param {Array} authenticating=true `true` if authentication is still in + * progress; `false` if it has already completed or failed. + * @param {Object} profile=null Topcoder user profile. + * @param {String} tokenV2='' Topcoder v2 auth token. + * @param {String} tokenV3='' Topcoder v3 auth token. + * @param {Object} user=null Topcoder user object (user information stored in + * v3 auth token). + */ + + +import { handleActions } from 'redux-actions'; +import actions from '../actions/notifications'; +import logger from '../utils/logger'; +import { fireErrorMessage } from '../utils/errors'; + + +/** + * Handles NOTIFICATIONS/GET_NOTIFICATIONS_INIT action. + * @param {Object} state + * @return {Object} New state + */ +function onGetNotificationsInit(state) { + return { ...state }; +} + +/** + * Handles NOTIFICATIONS/GET_NOTIFICATIONS_DONE action. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function onGetNotificationsDone(state, { error, payload }) { + if (error) { + logger.error('Failed to get notifications!', payload); + fireErrorMessage( + 'ERROR: Failed to load the notifications', + 'Please, try again a bit later', + ); + return { + ...state, + fetchNotificationsFailure: true, + items: [], + }; + } + + return { + ...state, + items: payload, + fetchNotificationsFailure: false, + }; +} + +/** + * Handles NOTIFICATIONS/MARK_NOTIFICATION_AS_INIT action. + * @param {Object} state + * @return {Object} New state + */ +function onMarkNotificationAsReadInit(state) { + return { ...state }; +} + +/** + * Handles NOTIFICATIONS/MARK_NOTIFICATION_AS_DONE action. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function onMarkNotificationAsReadDone(state, { error, payload }) { + if (error) { + logger.error('Failed to mark notification as read!', payload); + fireErrorMessage( + 'ERROR: Failed to mark the notification as read', + 'Please, try again a bit later', + ); + return { + ...state, + fetchNotificationsFailure: true, + }; + } + + const notifications = state.items; + const itemIndex = state.items.findIndex(item => item.id === payload.id); + notifications[itemIndex].isRead = true; + + return { + ...state, + fetchNotificationsFailure: false, + items: notifications, + }; +} + + +/** + * Handles NOTIFICATIONS/MARK_ALL_NOTIFICATION_AS_READ_INIT action. + * @param {Object} state + * @return {Object} New state + */ +function onMarkAllNotificationAsReadInit(state) { + return { ...state }; +} + +/** + * Handles NOTIFICATIONS/MARK_ALL_NOTIFICATION_AS_READ_DONE action. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function onMarkAllNotificationAsReadDone(state, { error, payload }) { + if (error) { + logger.error('Failed to mark notification as read!', payload); + fireErrorMessage( + 'ERROR: Failed to mark the notification as read', + 'Please, try again a bit later', + ); + return { + ...state, + fetchNotificationsFailure: true, + }; + } + + const notifications = state.items; + notifications.forEach((item, index) => { + notifications[index].isRead = true; + }); + + return { + ...state, + fetchNotificationsFailure: true, + items: notifications, + }; +} + +/** + * Handles NOTIFICATIONS/MARK_ALL_NOTIFICATION_AS_SEEN_INIT action. + * @param {Object} state + * @return {Object} New state + */ +function onMarkAllNotificationAsSeenInit(state) { + return { ...state }; +} + +/** + * Handles NOTIFICATIONS/MARK_ALL_NOTIFICATION_AS_SEEN_DONE action. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function onMarkAllNotificationAsSeenDone(state, { error, payload }) { + if (error) { + logger.error('Failed to mark notification as seen!', payload); + fireErrorMessage( + 'ERROR: Failed to mark the notification as seen', + 'Please, try again a bit later', + ); + return { + ...state, + fetchNotificationsFailure: true, + }; + } + + const items = payload.split('-'); + const notifications = state.items; + state.items.forEach((item, index) => { + if (items.includes(String(item.id))) { + notifications[index].isSeen = true; + } + }); + + return { + ...state, + fetchNotificationsFailure: false, + items: notifications, + }; +} + +/** + * Handles NOTIFICATIONS/DISMISS_CHALLENGE_NOTIFICATIONS_INIT action. + * @param {Object} state + * @return {Object} New state + */ +function onDismissChallengeNotificationsInit(state) { + return { ...state }; +} + +/** + * Handles NOTIFICATIONS/DISMISS_CHALLENGE_NOTIFICATIONS_DONE action. + * @param {Object} state + * @param {Object} action + * @return {Object} New state. + */ +function onDismissChallengeNotificationsDone(state, { error, payload }) { + if (error) { + logger.error('Failed to dismiss notification!', payload); + fireErrorMessage( + 'ERROR: Failed to dismiss the notification', + 'Please, try again a bit later', + ); + return { + ...state, + fetchNotificationsFailure: true, + items: [], + }; + } + + return { + ...state, + fetchNotificationsFailure: false, + }; +} + + +/** + * Creates a new Members reducer with the specified initial state. + * @param {Object} initialState Optional. Initial state. + * @return {Function} Members reducer. + */ +function create(initialState = {}) { + const a = actions.notifications; + return handleActions({ + [a.getNotificationsInit]: onGetNotificationsInit, + [a.getNotificationsDone]: onGetNotificationsDone, + [a.markNotificationAsReadInit]: onMarkNotificationAsReadInit, + [a.markNotificationAsReadDone]: onMarkNotificationAsReadDone, + [a.markAllNotificationAsReadInit]: onMarkAllNotificationAsReadInit, + [a.markAllNotificationAsReadDone]: onMarkAllNotificationAsReadDone, + [a.markAllNotificationAsSeenInit]: onMarkAllNotificationAsSeenInit, + [a.markAllNotificationAsSeenDone]: onMarkAllNotificationAsSeenDone, + [a.dismissChallengeNotificationsInit]: onDismissChallengeNotificationsInit, + [a.dismissChallengeNotificationsDone]: onDismissChallengeNotificationsDone, + }, initialState); +} + +/** + * Factory which creates a new reducer with its initial state tailored to the + * given options object, if specified (for server-side rendering). If options + * object is not specified, it creates just the default reducer. Accepted options are: + * @return {Promise} + * @resolves {Function(state, action): state} New reducer. + */ +export function factory() { + return Promise.resolve(create()); +} + +/** + * @static + * @member default + * @desc Reducer with default initial state. + */ +export default create(); diff --git a/src/services/index.js b/src/services/index.js index d6b5993b..76e2c457 100644 --- a/src/services/index.js +++ b/src/services/index.js @@ -16,6 +16,7 @@ import * as lookup from './lookup'; import * as userTraits from './user-traits'; import * as submissions from './submissions'; import * as memberSearch from './member-search'; +import * as notifications from './notifications'; export const services = { api, @@ -33,6 +34,7 @@ export const services = { userTraits, submissions, memberSearch, + notifications, }; export default undefined; diff --git a/src/services/notifications.js b/src/services/notifications.js new file mode 100644 index 00000000..94d3ec9d --- /dev/null +++ b/src/services/notifications.js @@ -0,0 +1,83 @@ +/** + * @module "services.notifications" + * @desc This module provides a service for searching for Topcoder + * notifications. + */ + +import { getApi } from './api'; + +/** + * Service class for Notifications. + */ +class NotificationService { + /** + * @param {String} tokenV3 Optional. Auth token for Topcoder API v5. + */ + constructor(tokenV3) { + this.private = { + apiV5: getApi('V5', tokenV3), + tokenV3, + }; + } + + /** + * Gets member's notification information. + * @return {Promise} Resolves to the notification information object. + */ + async getNotifications() { + return this.private.apiV5.get('/notifications/?platform=community&limit=20') + .then(res => (res.ok ? res.json() : new Error(res.statusText))); + } + + /** + * Marks given notification as read. + * @return {Promise} Resolves to the notification information object. + */ + async markNotificationAsRead(item) { + return this.private.apiV5.put(`/notifications/${item}/read`) + .then(res => (res.ok ? null : Promise.reject(new Error(res.statusText)))); + } + + /** + * Marks all notification as read. + * @return {Promise} Resolves to the notification information object. + */ + async markAllNotificationAsRead() { + return this.private.apiV5.put('/notifications/read') + .then(res => (res.ok ? null : Promise.reject(new Error(res.statusText)))); + } + + /** + * Marks all notification as seen. + * @return {Promise} Resolves to the notification information object. + */ + async markAllNotificationAsSeen(items) { + return this.private.apiV5.put(`/notifications/${items}/seen`) + .then(res => (res.ok ? null : Promise.reject(new Error(res.statusText)))); + } + + /** + * Dismiss challenge notifications. + * @return {Promise} Resolves to the notification information object. + */ + async dismissChallengeNotifications(challengeID) { + return this.private.apiV5.put(`/notifications/${challengeID}/dismiss`) + .then(res => (res.ok ? null : Promise.reject(new Error(res.statusText)))); + } +} + +let lastInstance = null; +/** + * Returns a new or existing notifications service. + * @param {String} tokenV3 Optional. Auth token for Topcoder API v3. + * @return {NotificationService} Notification service object + */ +export function getService(tokenV3) { + if (!lastInstance || tokenV3 !== lastInstance.private.tokenV3) { + lastInstance = new NotificationService(tokenV3); + } + return lastInstance; +} + +/* Using default export would be confusing in this case. */ +export default undefined;
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: