diff --git a/.gitignore b/.gitignore
index a6630d22e..5efed2e0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,6 +43,8 @@ plugins/*
!plugins/talk-plugin-notifications-category-featured
!plugins/talk-plugin-notifications-category-reply
!plugins/talk-plugin-notifications-category-staff
+!plugins/talk-plugin-notifications-digest-daily
+!plugins/talk-plugin-notifications-digest-hourly
!plugins/talk-plugin-offtopic
!plugins/talk-plugin-permalink
!plugins/talk-plugin-profile-settings
diff --git a/client/coral-admin/src/routes/Community/components/People.css b/client/coral-admin/src/routes/Community/components/People.css
index c4561ace1..669da480d 100644
--- a/client/coral-admin/src/routes/Community/components/People.css
+++ b/client/coral-admin/src/routes/Community/components/People.css
@@ -130,3 +130,12 @@ th.header:nth-child(2), th.header:nth-child(3) {
.loadMore {
margin-top: 24px;
}
+
+.roleDropdown {
+ width: 150px;
+}
+
+.roleOption {
+ min-width: 100px;
+}
+
diff --git a/client/coral-admin/src/routes/Community/components/People.js b/client/coral-admin/src/routes/Community/components/People.js
index 1f5e4bab8..5645f88f1 100644
--- a/client/coral-admin/src/routes/Community/components/People.js
+++ b/client/coral-admin/src/routes/Community/components/People.js
@@ -200,24 +200,31 @@ class People extends React.Component {
setUserRole(user.id, role)}
>
diff --git a/client/coral-admin/src/routes/Stories/components/Stories.css b/client/coral-admin/src/routes/Stories/components/Stories.css
index 63a05ab6a..28f67151d 100644
--- a/client/coral-admin/src/routes/Stories/components/Stories.css
+++ b/client/coral-admin/src/routes/Stories/components/Stories.css
@@ -89,3 +89,12 @@
display: inline-block;
}
}
+
+.statusDropdown {
+ width: 150px;
+}
+
+.statusDropdownOption {
+ min-width: 100px;
+}
+
diff --git a/client/coral-admin/src/routes/Stories/components/Stories.js b/client/coral-admin/src/routes/Stories/components/Stories.js
index f94685fe8..ad2c7c5fc 100644
--- a/client/coral-admin/src/routes/Stories/components/Stories.js
+++ b/client/coral-admin/src/routes/Stories/components/Stories.js
@@ -22,11 +22,20 @@ class Stories extends Component {
const closed = !!(closedAt && new Date(closedAt).getTime() < Date.now());
return (
this.props.onStatusChange(value, id)}
>
-
-
+
+
);
};
diff --git a/client/coral-framework/components/TalkProvider.js b/client/coral-framework/components/TalkProvider.js
index 343d559ea..9caba7fb8 100644
--- a/client/coral-framework/components/TalkProvider.js
+++ b/client/coral-framework/components/TalkProvider.js
@@ -10,6 +10,7 @@ class TalkProvider extends React.Component {
plugins: this.props.plugins,
rest: this.props.rest,
graphql: this.props.graphql,
+ introspection: this.props.introspection,
notification: this.props.notification,
localStorage: this.props.localStorage,
sessionStorage: this.props.sessionStorage,
@@ -29,6 +30,7 @@ class TalkProvider extends React.Component {
TalkProvider.childContextTypes = {
pym: PropTypes.object,
+ introspection: PropTypes.object,
eventEmitter: PropTypes.object,
plugins: PropTypes.object,
rest: PropTypes.func,
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index 5e2e3dd40..7ed114b73 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -11,6 +11,7 @@ export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export { default as withPopupAuthHandler } from './withPopupAuthHandler';
+export { default as withEnumValues } from './withEnumValues';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
diff --git a/client/coral-framework/hocs/withEnumValues.js b/client/coral-framework/hocs/withEnumValues.js
new file mode 100644
index 000000000..ea5cef922
--- /dev/null
+++ b/client/coral-framework/hocs/withEnumValues.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import hoistStatics from 'recompose/hoistStatics';
+import PropTypes from 'prop-types';
+
+/**
+ * WithEnumValues inject given property name for given enum.
+ */
+export default (enumName, propName) =>
+ hoistStatics(WrappedComponent => {
+ class WithEnumValues extends React.Component {
+ static contextTypes = {
+ introspection: PropTypes.object,
+ };
+
+ render() {
+ const inject = {
+ [propName]: this.context.introspection.getEnumValues(enumName),
+ };
+ return ;
+ }
+ }
+
+ return WithEnumValues;
+ });
diff --git a/client/coral-framework/services/introspection.js b/client/coral-framework/services/introspection.js
index 8e3160aa7..e4636a339 100644
--- a/client/coral-framework/services/introspection.js
+++ b/client/coral-framework/services/introspection.js
@@ -19,6 +19,16 @@ class Introspection {
isValidEnumValue(name, value) {
return this._enums[name] && this._enums[name].indexOf(value) >= 0;
}
+
+ /**
+ * getEnumValues returns array of possible values.
+ * @param {string} name
+ * @param {string} value
+ * @return {array}
+ */
+ getEnumValues(name) {
+ return this._enums[name];
+ }
}
/**
diff --git a/client/coral-ui/components/Dropdown.css b/client/coral-ui/components/Dropdown.css
index 794955220..c17e6322a 100644
--- a/client/coral-ui/components/Dropdown.css
+++ b/client/coral-ui/components/Dropdown.css
@@ -1,20 +1,27 @@
.dropdown {
position: relative;
- width: 150px;
height: 34px;
background: #2c2c2c;
box-sizing: border-box;
color: white;
border-radius: 3px;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
+ line-height: 20px;
font-size: 0.98em;
border-radius: 3px;
+ cursor: pointer;
+
+ &.disabled {
+ color: #e5e5e5;
+ background: #888;
+ cursor: default;
+ pointer-events: none;
+ }
}
.toggle {
- padding: 8px 15px;
- cursor: pointer;
+ padding: 8px 45px 8px 15px;
outline: none;
&:focus {
@@ -27,7 +34,6 @@
}
.label {
- text-transform: capitalize;
}
.list {
@@ -38,7 +44,6 @@
border-radius: 2px;
background: white;
box-shadow: 0 2px 2px 0 rgba(0,0,0,.14), 0 3px 1px -2px rgba(0,0,0,.2), 0 1px 5px 0 rgba(0,0,0,.12);
- width: 100%;
list-style: none;
padding: 0;
margin: 0;
diff --git a/client/coral-ui/components/Dropdown.js b/client/coral-ui/components/Dropdown.js
index fe852d265..bf885477f 100644
--- a/client/coral-ui/components/Dropdown.js
+++ b/client/coral-ui/components/Dropdown.js
@@ -13,6 +13,12 @@ class Dropdown extends React.Component {
isOpen: false,
};
+ componentWillReceiveProps(nextProps) {
+ if (this.state.isOpen && nextProps.disabled) {
+ this.toggle();
+ }
+ }
+
componentDidUpdate(_, prevState) {
if (!this.state.isOpen && prevState.isOpen) {
// Refocus on the toggle element when menu closes.
@@ -69,6 +75,10 @@ class Dropdown extends React.Component {
};
toggle = () => {
+ if (this.props.disabled) {
+ return false;
+ }
+
this.setState({
isOpen: !this.state.isOpen,
});
@@ -135,11 +145,21 @@ class Dropdown extends React.Component {
containerClassName,
toggleClassName,
toggleOpenClassName,
+ disabled,
+ className,
} = this.props;
return (
{this.props.icon && (
@@ -206,6 +226,7 @@ class Dropdown extends React.Component {
}
Dropdown.propTypes = {
+ className: PropTypes.string,
containerClassName: PropTypes.string,
toggleClassName: PropTypes.string,
toggleOpenClassName: PropTypes.string,
@@ -213,6 +234,7 @@ Dropdown.propTypes = {
icon: PropTypes.string,
onChange: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
+ disabled: PropTypes.bool,
value: PropTypes.oneOfType([
PropTypes.number,
PropTypes.string,
diff --git a/client/coral-ui/components/Option.css b/client/coral-ui/components/Option.css
index 1e7627681..892bc4e7c 100644
--- a/client/coral-ui/components/Option.css
+++ b/client/coral-ui/components/Option.css
@@ -1,7 +1,7 @@
.option {
padding: 10px;
- text-transform: capitalize;
outline: none;
+ white-space: nowrap;
&:focus, &:hover {
background-color: #ccc;
diff --git a/plugin-api/beta/client/hocs/index.js b/plugin-api/beta/client/hocs/index.js
index 8b91c83de..35899a418 100644
--- a/plugin-api/beta/client/hocs/index.js
+++ b/plugin-api/beta/client/hocs/index.js
@@ -12,6 +12,7 @@ export {
withSignUp,
withResendEmailConfirmation,
withSetUsername,
+ withEnumValues,
} from 'coral-framework/hocs';
export {
withIgnoreUser,
diff --git a/plugins/talk-plugin-notifications-category-featured/index.js b/plugins/talk-plugin-notifications-category-featured/index.js
index c6b0ea2bb..bd90db3ea 100644
--- a/plugins/talk-plugin-notifications-category-featured/index.js
+++ b/plugins/talk-plugin-notifications-category-featured/index.js
@@ -84,6 +84,7 @@ const handler = {
category: 'featured',
event: 'commentFeatured',
hydrate,
+ digestOrder: 10,
};
module.exports = {
diff --git a/plugins/talk-plugin-notifications-category-featured/translations.yml b/plugins/talk-plugin-notifications-category-featured/translations.yml
index bd0d7412f..18b00ea71 100644
--- a/plugins/talk-plugin-notifications-category-featured/translations.yml
+++ b/plugins/talk-plugin-notifications-category-featured/translations.yml
@@ -2,5 +2,5 @@ en:
talk-plugin-notifications:
categories:
featured:
- subject: "One of your comments was featured on [{0}]"
+ subject: "One of your comments was featured on {0}"
body: "{0}\nA member of our team has selected this comment to be featured for other readers: {1}"
\ No newline at end of file
diff --git a/plugins/talk-plugin-notifications-category-reply/index.js b/plugins/talk-plugin-notifications-category-reply/index.js
index 16cfbfb72..0b6fe62a0 100644
--- a/plugins/talk-plugin-notifications-category-reply/index.js
+++ b/plugins/talk-plugin-notifications-category-reply/index.js
@@ -90,7 +90,13 @@ const hydrate = async (ctx, category, context) => {
return [headline, replier, permalink];
};
-const handler = { handle, category: 'reply', event: 'commentAdded', hydrate };
+const handler = {
+ handle,
+ category: 'reply',
+ event: 'commentAdded',
+ hydrate,
+ digestOrder: 30,
+};
module.exports = {
typeDefs: `
diff --git a/plugins/talk-plugin-notifications-category-staff/index.js b/plugins/talk-plugin-notifications-category-staff/index.js
index d229456f7..a5d12c175 100644
--- a/plugins/talk-plugin-notifications-category-staff/index.js
+++ b/plugins/talk-plugin-notifications-category-staff/index.js
@@ -117,6 +117,7 @@ const handler = {
event: 'commentAdded',
hydrate,
supersedesCategories: ['reply'],
+ digestOrder: 20,
};
module.exports = {
diff --git a/plugins/talk-plugin-notifications-digest-daily/client/.eslintrc.json b/plugins/talk-plugin-notifications-digest-daily/client/.eslintrc.json
new file mode 100644
index 000000000..c8a6db18a
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-daily/client/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@coralproject/eslint-config-talk/client"
+}
diff --git a/plugins/talk-plugin-notifications-digest-daily/client/index.js b/plugins/talk-plugin-notifications-digest-daily/client/index.js
new file mode 100644
index 000000000..3ef3337ee
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-daily/client/index.js
@@ -0,0 +1,7 @@
+import translations from './translations.yml';
+
+export default {
+ // The only thing necessary on the client side is to provide
+ // a translation for the new enum.
+ translations,
+};
diff --git a/plugins/talk-plugin-notifications-digest-daily/client/translations.yml b/plugins/talk-plugin-notifications-digest-daily/client/translations.yml
new file mode 100644
index 000000000..1744918ec
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-daily/client/translations.yml
@@ -0,0 +1,4 @@
+en:
+ talk-plugin-notifications:
+ digest_enum:
+ DAILY: In a daily digest
diff --git a/plugins/talk-plugin-notifications-digest-daily/index.js b/plugins/talk-plugin-notifications-digest-daily/index.js
new file mode 100644
index 000000000..a06558e33
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-daily/index.js
@@ -0,0 +1,11 @@
+module.exports = {
+ typeDefs: `
+ enum DIGEST_FREQUENCY {
+ # DAILY will queue up the notifications and send them daily.
+ DAILY
+ }
+ `,
+ notificationDigests: {
+ DAILY: { cronTime: '0 0 * * *', timeZone: 'America/New_York' },
+ },
+};
diff --git a/plugins/talk-plugin-notifications-digest-hourly/client/.eslintrc.json b/plugins/talk-plugin-notifications-digest-hourly/client/.eslintrc.json
new file mode 100644
index 000000000..c8a6db18a
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-hourly/client/.eslintrc.json
@@ -0,0 +1,3 @@
+{
+ "extends": "@coralproject/eslint-config-talk/client"
+}
diff --git a/plugins/talk-plugin-notifications-digest-hourly/client/index.js b/plugins/talk-plugin-notifications-digest-hourly/client/index.js
new file mode 100644
index 000000000..3ef3337ee
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-hourly/client/index.js
@@ -0,0 +1,7 @@
+import translations from './translations.yml';
+
+export default {
+ // The only thing necessary on the client side is to provide
+ // a translation for the new enum.
+ translations,
+};
diff --git a/plugins/talk-plugin-notifications-digest-hourly/client/translations.yml b/plugins/talk-plugin-notifications-digest-hourly/client/translations.yml
new file mode 100644
index 000000000..da4bad14f
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-hourly/client/translations.yml
@@ -0,0 +1,4 @@
+en:
+ talk-plugin-notifications:
+ digest_enum:
+ HOURLY: In an hourly digest
diff --git a/plugins/talk-plugin-notifications-digest-hourly/index.js b/plugins/talk-plugin-notifications-digest-hourly/index.js
new file mode 100644
index 000000000..50c53faf6
--- /dev/null
+++ b/plugins/talk-plugin-notifications-digest-hourly/index.js
@@ -0,0 +1,11 @@
+module.exports = {
+ typeDefs: `
+ enum DIGEST_FREQUENCY {
+ # HOURLY will queue up the notifications and send them hourly.
+ HOURLY
+ }
+ `,
+ notificationDigests: {
+ HOURLY: { cronTime: '0 * * * *', timeZone: 'America/New_York' },
+ },
+};
diff --git a/plugins/talk-plugin-notifications/client/components/Settings.css b/plugins/talk-plugin-notifications/client/components/Settings.css
index c4d43fd9a..f9a65c88d 100644
--- a/plugins/talk-plugin-notifications/client/components/Settings.css
+++ b/plugins/talk-plugin-notifications/client/components/Settings.css
@@ -1,6 +1,6 @@
.root {
margin-bottom: 20px;
- width: 380px;
+ width: 350px;
}
.innerSettings {
@@ -30,3 +30,16 @@
.disabled {
color: #e5e5e5;
}
+
+.digest {
+ display: flex;
+ align-items: center;
+}
+
+.titleDigest {
+ width: 100%;
+}
+
+.digestDropDown {
+ flex-shrink: 0;
+}
diff --git a/plugins/talk-plugin-notifications/client/components/Settings.js b/plugins/talk-plugin-notifications/client/components/Settings.js
index 010422491..a5c3b4fcd 100644
--- a/plugins/talk-plugin-notifications/client/components/Settings.js
+++ b/plugins/talk-plugin-notifications/client/components/Settings.js
@@ -4,7 +4,11 @@ import { IfSlotIsNotEmpty } from 'plugin-api/beta/client/components';
import { Slot } from 'plugin-api/beta/client/components';
import { t } from 'plugin-api/beta/client/services';
import styles from './Settings.css';
-import { BareButton } from 'plugin-api/beta/client/components/ui';
+import {
+ BareButton,
+ Dropdown,
+ Option,
+} from 'plugin-api/beta/client/components/ui';
import EmailVerificationBanner from '../containers/EmailVerificationBanner';
import cn from 'classnames';
@@ -24,16 +28,29 @@ class Settings extends React.Component {
setTurnOffInputFragment,
updateNotificationSettings,
turnOffAll,
- turnOffButtonDisabled,
needEmailVerification,
email,
+ digestFrequencyValues,
+ digestFrequency,
+ disableDigest,
+ disableTurnoffButton,
+ onChangeDigestFrequency,
} = this.props;
+ const slotProps = {
+ queryData: { root },
+ setTurnOffInputFragment: setTurnOffInputFragment,
+ updateNotificationSettings: updateNotificationSettings,
+ disabled: needEmailVerification,
+ };
+
return (
-
+
{t('talk-plugin-notifications.settings_title')}
- {needEmailVerification && }
+
+ {needEmailVerification && }
+
-
- {t('talk-plugin-notifications.turn_off_all')}
-
+ {digestFrequencyValues.length > 1 && (
+
+
+ {t('talk-plugin-notifications.digest_option')}
+
+
+ {digestFrequencyValues.map(v => (
+
+ ))}
+
+
+ )}
+
+ {t('talk-plugin-notifications.turn_off_all')}
+
);
@@ -72,9 +111,13 @@ Settings.propTypes = {
setTurnOffInputFragment: PropTypes.func.isRequired,
updateNotificationSettings: PropTypes.func.isRequired,
turnOffAll: PropTypes.func.isRequired,
- turnOffButtonDisabled: PropTypes.bool.isRequired,
+ disableTurnoffButton: PropTypes.bool.isRequired,
+ disableDigest: PropTypes.bool.isRequired,
needEmailVerification: PropTypes.bool.isRequired,
email: PropTypes.string,
+ digestFrequencyValues: PropTypes.array.isRequired,
+ digestFrequency: PropTypes.string.isRequired,
+ onChangeDigestFrequency: PropTypes.func.isRequired,
};
export default Settings;
diff --git a/plugins/talk-plugin-notifications/client/containers/Settings.js b/plugins/talk-plugin-notifications/client/containers/Settings.js
index 76a03462d..f3e6c7e6d 100644
--- a/plugins/talk-plugin-notifications/client/containers/Settings.js
+++ b/plugins/talk-plugin-notifications/client/containers/Settings.js
@@ -2,7 +2,11 @@ import React from 'react';
import PropTypes from 'prop-types';
import { compose, gql } from 'react-apollo';
import Settings from '../components/Settings';
-import { withFragments, excludeIf } from 'plugin-api/beta/client/hocs';
+import {
+ withFragments,
+ excludeIf,
+ withEnumValues,
+} from 'plugin-api/beta/client/hocs';
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
import { withUpdateNotificationSettings } from '../mutations';
@@ -33,6 +37,10 @@ class SettingsContainer extends React.Component {
this.props.updateNotificationSettings(this.state.turnOffInput);
};
+ setDigestFrequency = digestFrequency => {
+ this.props.updateNotificationSettings({ digestFrequency });
+ };
+
getNeedEmailVerification() {
return !this.props.root.me.profiles.some(
profile => profile.provider === 'local' && profile.confirmedAt
@@ -49,9 +57,15 @@ class SettingsContainer extends React.Component {
setTurnOffInputFragment={this.setTurnOffInputFragment}
updateNotificationSettings={this.props.updateNotificationSettings}
turnOffAll={this.turnOffAll}
- turnOffButtonDisabled={this.state.hasNotifications.length === 0}
needEmailVerification={this.getNeedEmailVerification()}
email={this.props.root.me.email}
+ digestFrequencyValues={this.props.digestFrequencyValues}
+ digestFrequency={
+ this.props.root.me.notificationSettings.digestFrequency
+ }
+ disableDigest={this.state.hasNotifications.length === 0}
+ disableTurnoffButton={this.state.hasNotifications.length === 0}
+ onChangeDigestFrequency={this.setDigestFrequency}
/>
);
}
@@ -61,6 +75,7 @@ SettingsContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
updateNotificationSettings: PropTypes.func.isRequired,
+ digestFrequencyValues: PropTypes.array.isRequired,
};
const enhance = compose(
@@ -70,6 +85,9 @@ const enhance = compose(
__typename
${getSlotFragmentSpreads(slots, 'root')}
me {
+ notificationSettings {
+ digestFrequency
+ }
email
profiles {
provider
@@ -85,7 +103,8 @@ const enhance = compose(
props =>
!props.root.me.profiles.some(profile => profile.provider === 'local')
),
- withUpdateNotificationSettings
+ withUpdateNotificationSettings,
+ withEnumValues('DIGEST_FREQUENCY', 'digestFrequencyValues')
);
export default enhance(SettingsContainer);
diff --git a/plugins/talk-plugin-notifications/client/graphql.js b/plugins/talk-plugin-notifications/client/graphql.js
index b48b63668..e99bc6a83 100644
--- a/plugins/talk-plugin-notifications/client/graphql.js
+++ b/plugins/talk-plugin-notifications/client/graphql.js
@@ -11,13 +11,38 @@ export default {
`,
},
mutations: {
- UpdateNotificationSettings: () => ({
+ UpdateNotificationSettings: ({
+ variables: { input },
+ state: { auth: { user: { id } } },
+ }) => ({
optimisticResponse: {
updateNotificationSettings: {
__typename: 'UpdateNotificationSettingsResponse',
errors: null,
},
},
+ update: proxy => {
+ if (input.digestFrequency === undefined) {
+ return;
+ }
+
+ const fragment = gql`
+ fragment TalkNotificationsCategoryReply_User_Fragment on User {
+ notificationSettings {
+ digestFrequency
+ }
+ }
+ `;
+ const fragmentId = `User_${id}`;
+ const data = {
+ __typename: 'User',
+ notificationSettings: {
+ __typename: 'NotificationSettings',
+ digestFrequency: input.digestFrequency,
+ },
+ };
+ proxy.writeFragment({ fragment, id: fragmentId, data });
+ },
}),
},
};
diff --git a/plugins/talk-plugin-notifications/client/translations.yml b/plugins/talk-plugin-notifications/client/translations.yml
index 0d74114d2..3ed123502 100644
--- a/plugins/talk-plugin-notifications/client/translations.yml
+++ b/plugins/talk-plugin-notifications/client/translations.yml
@@ -13,3 +13,6 @@ en:
banner_error:
title: Error
text: There has been an error sending your verification email. Please try again later.
+ digest_option: Send notifications
+ digest_enum:
+ NONE: Immediately
diff --git a/plugins/talk-plugin-notifications/package.json b/plugins/talk-plugin-notifications/package.json
index fbf9657fb..1ec166cc8 100644
--- a/plugins/talk-plugin-notifications/package.json
+++ b/plugins/talk-plugin-notifications/package.json
@@ -6,6 +6,7 @@
"license": "Apache-2.0",
"private": false,
"dependencies": {
+ "cron": "^1.3.0",
"linkifyjs": "^2.1.5"
}
}
diff --git a/plugins/talk-plugin-notifications/server/NotificationManager.js b/plugins/talk-plugin-notifications/server/NotificationManager.js
index 6b53d33a7..2d901de55 100644
--- a/plugins/talk-plugin-notifications/server/NotificationManager.js
+++ b/plugins/talk-plugin-notifications/server/NotificationManager.js
@@ -1,10 +1,24 @@
-const { get, find, groupBy, forEach, property } = require('lodash');
-const debug = require('debug')('talk-plugin-notifications');
-const uuid = require('uuid/v4');
const {
- UNSUBSCRIBE_SUBJECT,
- DISABLE_REQUIRE_EMAIL_VERIFICATIONS,
-} = require('./config');
+ merge,
+ map,
+ get,
+ find,
+ groupBy,
+ forEach,
+ flatten,
+ property,
+} = require('lodash');
+const debug = require('debug')('talk-plugin-notifications');
+const { DISABLE_REQUIRE_EMAIL_VERIFICATIONS } = require('./config');
+const { CronJob } = require('cron');
+const { getOrganizationName } = require('./util');
+const {
+ processNewNotifications,
+ filterSuperseded,
+ filterVerified,
+ sendNotification,
+} = require('./messages');
+const { renderDigestMessage } = require('./digests');
// handleHandlers will call the handle method on each handler to determine if a
// notification should be sent for it.
@@ -32,97 +46,11 @@ const handleHandlers = (ctx, handlers, ...args) =>
})
);
-// filterSuperseded will filter all the possible notifications and only send
-// those notifications that are not superseded by another type of notification.
-const filterSuperseded = (
- { handler: { category }, notification: { userID: destinationUserID } },
- index,
- notifications
-) =>
- !notifications.some(
- ({
- handler: { supersedesCategories = [] },
- notification: { userID: notificationUserID },
- }) =>
- // Only allow notifications to supersede another notification if that
- // notification is also destined for the same user.
- notificationUserID === destinationUserID &&
- // If another notification that is destined for the same user also exists
- // and declares that it supersedes this one, return true so we can filter
- // this one from the list.
- supersedesCategories.some(
- supersededCategory => supersededCategory === category
- )
- );
-
-const USER_CONFIRMATION_QUERY = `
- query CheckUserConfirmation($userID: ID!) {
- user(id: $userID) {
- profiles {
- provider
- ... on LocalUserProfile {
- confirmedAt
- }
- }
- }
- }
-`;
-
-// filterVerifiedNotification checks to see if a user has a verified email
-// address, and if they do, returns the notification payload again, otherwise,
-// returns undefined.
-const filterVerifiedNotification = ctx => async notification => {
- // Grab the user that we're supposed to be sending the notification to.
- const { notification: { userID } } = notification;
-
- // Check their confirmed status. This should have already been hit by the
- // loaders, so we shouldn't make any more database requests.
- const { errors, data } = await ctx.graphql(USER_CONFIRMATION_QUERY, {
- userID,
- });
- if (errors) {
- ctx.log.error(
- { err: errors },
- 'could not query for user confirmation status'
- );
- return;
- }
-
- // Get the first local profile from the user.
- const profile = find(get(data, 'user.profiles', []), ['provider', 'local']);
- if (!profile) {
- ctx.log.warn({ user_id: userID }, 'user did not have a local profile');
- return;
- }
-
- // Pull out the confirmed status from the profile.
- const confirmed = get(profile, 'confirmedAt', null) !== null;
- if (!confirmed) {
- ctx.log.info(
- { user_id: userID },
- 'user did not have their local profile confirmed, but had settings enabled, not mailing'
- );
- return;
- }
-
- return notification;
-};
-
-// filterVerified performs filtering in a complicated way because we can't use
-// Promise.all on a Array.prototype.filter call.
-const filterVerified = async (ctx, notifications) => {
- notifications = await Promise.all(
- notifications.map(filterVerifiedNotification(ctx))
- );
-
- // This acts as a poor-mans identity filter to remove all falsy values.
- return notifications.filter(property('notification'));
-};
-
class NotificationManager {
constructor(context) {
this.context = context;
this.registry = [];
+ this.digests = [];
}
/**
@@ -149,17 +77,170 @@ class NotificationManager {
.map(({ category }) => category)
.join(', ')}] handlers when the '${event}' event is emitted`
);
- broker.on(event, this.handle(handlers));
+ broker.on(event, this.handleUserEvent(handlers));
});
}
/**
- * handle will wrap a notification handler and attach it to the notification
- * stream system.
+ * registerDigests will register the digest handlers.
+ *
+ * @param {Array |