@@ -84,6 +86,17 @@ class ColumnSettings extends React.PureComponent {
+ {supportsBirthDates &&
+
diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js
index da37f306f..05dc1f0eb 100644
--- a/app/soapbox/features/notifications/containers/column_settings_container.js
+++ b/app/soapbox/features/notifications/containers/column_settings_container.js
@@ -24,6 +24,7 @@ const mapStateToProps = state => {
settings: getSettings(state).get('notifications'),
pushSettings: state.get('push_notifications'),
supportsEmojiReacts: features.emojiReacts,
+ supportsBirthDates: features.birthDates,
};
};
diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js
index b174d776e..57edf8315 100644
--- a/app/soapbox/features/notifications/index.js
+++ b/app/soapbox/features/notifications/index.js
@@ -8,8 +8,10 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { getSettings } from 'soapbox/actions/settings';
+import BirthdayReminders from 'soapbox/components/birthday_reminders';
import SubNavigation from 'soapbox/components/sub_navigation';
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
+import { getFeatures } from 'soapbox/utils/features';
import {
expandNotifications,
@@ -45,14 +47,21 @@ const getNotifications = createSelector([
return notifications.filter(item => item !== null && allowedType === item.get('type'));
});
-const mapStateToProps = state => ({
- showFilterBar: getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
- notifications: getNotifications(state),
- isLoading: state.getIn(['notifications', 'isLoading'], true),
- isUnread: state.getIn(['notifications', 'unread']) > 0,
- hasMore: state.getIn(['notifications', 'hasMore']),
- totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
-});
+const mapStateToProps = state => {
+ const settings = getSettings(state);
+ const instance = state.get('instance');
+ const features = getFeatures(instance);
+
+ return {
+ showFilterBar: settings.getIn(['notifications', 'quickFilter', 'show']),
+ notifications: getNotifications(state),
+ isLoading: state.getIn(['notifications', 'isLoading'], true),
+ isUnread: state.getIn(['notifications', 'unread']) > 0,
+ hasMore: state.getIn(['notifications', 'hasMore']),
+ totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
+ showBirthdayReminders: settings.getIn(['notifications', 'birthdays', 'show']) && settings.getIn(['notifications', 'quickFilter', 'active']) === 'all' && features.birthDates,
+ };
+};
export default @connect(mapStateToProps)
@injectIntl
@@ -68,6 +77,7 @@ class Notifications extends React.PureComponent {
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
+ showBirthdayReminders: PropTypes.bool,
};
componentWillUnmount() {
@@ -137,7 +147,7 @@ class Notifications extends React.PureComponent {
}
render() {
- const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
+ const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount, showBirthdayReminders } = this.props;
const emptyMessage =
;
let scrollableContent = null;
@@ -164,6 +174,8 @@ class Notifications extends React.PureComponent {
onMoveDown={this.handleMoveDown}
/>
));
+
+ if (showBirthdayReminders) scrollableContent = scrollableContent.unshift(
);
} else {
scrollableContent = null;
}
diff --git a/app/soapbox/features/ui/components/birthdays_modal.js b/app/soapbox/features/ui/components/birthdays_modal.js
new file mode 100644
index 000000000..995ad5cc5
--- /dev/null
+++ b/app/soapbox/features/ui/components/birthdays_modal.js
@@ -0,0 +1,97 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { injectIntl, FormattedMessage, defineMessages } from 'react-intl';
+import { connect } from 'react-redux';
+
+import IconButton from 'soapbox/components/icon_button';
+import LoadingIndicator from 'soapbox/components/loading_indicator';
+import ScrollableList from 'soapbox/components/scrollable_list';
+import AccountContainer from 'soapbox/containers/account_container';
+
+const messages = defineMessages({
+ close: { id: 'lightbox.close', defaultMessage: 'Close' },
+});
+
+const mapStateToProps = (state) => {
+ const me = state.get('me');
+
+ return {
+ accountIds: state.getIn(['user_lists', 'birthday_reminders', me]),
+ };
+};
+
+export default @connect(mapStateToProps)
+@injectIntl
+class BirthdaysModal extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ onClose: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ accountIds: ImmutablePropTypes.orderedSet,
+ };
+
+ componentDidMount() {
+ this.unlistenHistory = this.context.router.history.listen((_, action) => {
+ if (action === 'PUSH') {
+ this.onClickClose(null, true);
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ if (this.unlistenHistory) {
+ this.unlistenHistory();
+ }
+ }
+
+ onClickClose = (_, noPop) => {
+ this.props.onClose('BIRTHDAYS', noPop);
+ };
+
+ render() {
+ const { intl, accountIds } = this.props;
+
+ let body;
+
+ if (!accountIds) {
+ body =
;
+ } else {
+ const emptyMessage =
;
+
+ body = (
+
+ {accountIds.map(id =>
+ ,
+ )}
+
+ );
+ }
+
+
+ return (
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js
index 5425cf5eb..1cc6304c6 100644
--- a/app/soapbox/features/ui/components/modal_root.js
+++ b/app/soapbox/features/ui/components/modal_root.js
@@ -26,6 +26,7 @@ import {
FavouritesModal,
ReblogsModal,
MentionsModal,
+ BirthdaysModal,
} from '../../../features/ui/util/async-components';
import BundleContainer from '../containers/bundle_container';
@@ -57,6 +58,7 @@ const MODAL_COMPONENTS = {
'FAVOURITES': FavouritesModal,
'REACTIONS': ReactionsModal,
'MENTIONS': MentionsModal,
+ 'BIRTHDAYS': BirthdaysModal,
};
export default class ModalRoot extends React.PureComponent {
diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js
index 08a223f90..7111a43c5 100644
--- a/app/soapbox/features/ui/util/async-components.js
+++ b/app/soapbox/features/ui/util/async-components.js
@@ -214,6 +214,10 @@ export function MentionsModal() {
return import(/* webpackChunkName: "features/ui" */'../components/mentions_modal');
}
+export function BirthdaysModal() {
+ return import(/* webpackChunkName: "features/ui" */'../components/birthdays_modal');
+}
+
export function ListEditor() {
return import(/* webpackChunkName: "features/list_editor" */'../../list_editor');
}
diff --git a/app/soapbox/reducers/user_lists.js b/app/soapbox/reducers/user_lists.js
index 2b9feaaa2..076144f75 100644
--- a/app/soapbox/reducers/user_lists.js
+++ b/app/soapbox/reducers/user_lists.js
@@ -10,6 +10,7 @@ import {
FOLLOW_REQUEST_AUTHORIZE_SUCCESS,
FOLLOW_REQUEST_REJECT_SUCCESS,
PINNED_ACCOUNTS_FETCH_SUCCESS,
+ BIRTHDAY_REMINDERS_FETCH_SUCCESS,
} from '../actions/accounts';
import {
BLOCKS_FETCH_SUCCESS,
@@ -55,6 +56,7 @@ const initialState = ImmutableMap({
groups: ImmutableMap(),
groups_removed_accounts: ImmutableMap(),
pinned: ImmutableMap(),
+ birthday_reminders: ImmutableMap(),
});
const normalizeList = (state, type, id, accounts, next) => {
@@ -131,6 +133,8 @@ export default function userLists(state = initialState, action) {
return state.updateIn(['groups_removed_accounts', action.groupId, 'items'], list => list.filterNot(item => item === action.id));
case PINNED_ACCOUNTS_FETCH_SUCCESS:
return normalizeList(state, 'pinned', action.id, action.accounts, action.next);
+ case BIRTHDAY_REMINDERS_FETCH_SUCCESS:
+ return state.setIn(['birthday_reminders', action.id], ImmutableOrderedSet(action.accounts.map(item => item.id)));
default:
return state;
}
diff --git a/app/styles/components/notification.scss b/app/styles/components/notification.scss
index 0a1e58a09..1bcbb937c 100644
--- a/app/styles/components/notification.scss
+++ b/app/styles/components/notification.scss
@@ -89,3 +89,18 @@
padding-bottom: 8px !important;
}
}
+
+.notification-birthday span[type="button"] {
+ &:focus,
+ &:hover,
+ &:active {
+ text-decoration: underline;
+ cursor: pointer;
+ }
+}
+
+.columns-area .notification-birthday {
+ .notification__message {
+ padding-top: 0;
+ }
+}
diff --git a/docs/store.md b/docs/store.md
index 34088ab8c..7f866bacb 100644
--- a/docs/store.md
+++ b/docs/store.md
@@ -391,6 +391,9 @@ If it's not documented, it's because I inherited it from Mastodon and I don't kn
mention: true,
poll: true,
reblog: true
+ },
+ birthdays: {
+ show: true
}
},
theme: 'azure',