diff --git a/app/soapbox/features/notifications/components/column_settings.js b/app/soapbox/features/notifications/components/column_settings.js
index e033eb6be..63093ec78 100644
--- a/app/soapbox/features/notifications/components/column_settings.js
+++ b/app/soapbox/features/notifications/components/column_settings.js
@@ -24,6 +24,7 @@ class ColumnSettings extends React.PureComponent {
onClear: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
supportsEmojiReacts: PropTypes.bool,
+ supportsBirthdays: PropTypes.bool,
};
onPushChange = (path, checked) => {
@@ -39,7 +40,7 @@ class ColumnSettings extends React.PureComponent {
}
render() {
- const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts } = this.props;
+ const { intl, settings, pushSettings, onChange, onClear, onClose, supportsEmojiReacts, supportsBirthdays } = this.props;
const filterShowStr =
;
const filterAdvancedStr =
;
@@ -50,6 +51,7 @@ class ColumnSettings extends React.PureComponent {
const soundSettings = [['sounds', 'follow'], ['sounds', 'favourite'], ['sounds', 'pleroma:emoji_reaction'], ['sounds', 'mention'], ['sounds', 'reblog'], ['sounds', 'poll'], ['sounds', 'move']];
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings &&
;
+ const birthdaysStr =
;
return (
@@ -84,6 +86,17 @@ class ColumnSettings extends React.PureComponent {
diff --git a/app/soapbox/features/notifications/containers/column_settings_container.js b/app/soapbox/features/notifications/containers/column_settings_container.js
index da37f306f..292c08961 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,
+ supportsBirthdays: features.birthdays,
};
};
diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js
index b174d776e..b3afd7c2f 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,24 @@ 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);
+ const showBirthdayReminders = settings.getIn(['notifications', 'birthdays', 'show']) && settings.getIn(['notifications', 'quickFilter', 'active']) === 'all' && features.birthdays;
+ const birthdays = showBirthdayReminders && state.getIn(['user_lists', 'birthday_reminders', state.get('me')]);
+
+ 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,
+ hasBirthdays: !!birthdays,
+ };
+};
export default @connect(mapStateToProps)
@injectIntl
@@ -68,6 +80,8 @@ class Notifications extends React.PureComponent {
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
+ showBirthdayReminders: PropTypes.bool,
+ hasBirthdays: PropTypes.bool,
};
componentWillUnmount() {
@@ -104,15 +118,25 @@ class Notifications extends React.PureComponent {
}
handleMoveUp = id => {
- const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+ const { hasBirthdays } = this.props;
+
+ let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
+ if (hasBirthdays) elementIndex++;
this._selectChild(elementIndex, true);
}
handleMoveDown = id => {
- const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+ const { hasBirthdays } = this.props;
+
+ let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
+ if (hasBirthdays) elementIndex++;
this._selectChild(elementIndex, false);
}
+ handleMoveBelowBirthdays = () => {
+ this._selectChild(1, false);
+ }
+
_selectChild(index, align_top) {
const container = this.column.node;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
@@ -137,7 +161,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 +188,13 @@ 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..9a0744ba7
--- /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 Account from 'soapbox/features/birthdays/account';
+
+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/components/profile_info_panel.js b/app/soapbox/features/ui/components/profile_info_panel.js
index 74dd9a187..6b35c601e 100644
--- a/app/soapbox/features/ui/components/profile_info_panel.js
+++ b/app/soapbox/features/ui/components/profile_info_panel.js
@@ -80,6 +80,41 @@ class ProfileInfoPanel extends ImmutablePureComponent {
return badges;
}
+ getBirthday = () => {
+ const { account, intl } = this.props;
+
+ const birthday = account.getIn(['pleroma', 'birthday']);
+ if (!birthday) return null;
+
+ const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'long', year: 'numeric' });
+
+ const date = new Date(birthday);
+ const today = new Date();
+
+ const hasBirthday = date.getDate() === today.getDate() && date.getMonth() === today.getMonth();
+
+ if (hasBirthday) {
+ return (
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+ );
+ }
+
render() {
const { account, displayFqn, intl, identity_proofs, username } = this.props;
@@ -150,6 +185,8 @@ class ProfileInfoPanel extends ImmutablePureComponent {
/>