Merge branch 'birthdays' into 'develop'
Birth dates See merge request soapbox-pub/soapbox-fe!1000
This commit is contained in:
commit
3708c0128c
25 changed files with 768 additions and 33 deletions
|
@ -109,6 +109,10 @@ export const NOTIFICATION_SETTINGS_REQUEST = 'NOTIFICATION_SETTINGS_REQUEST';
|
|||
export const NOTIFICATION_SETTINGS_SUCCESS = 'NOTIFICATION_SETTINGS_SUCCESS';
|
||||
export const NOTIFICATION_SETTINGS_FAIL = 'NOTIFICATION_SETTINGS_FAIL';
|
||||
|
||||
export const BIRTHDAY_REMINDERS_FETCH_REQUEST = 'BIRTHDAY_REMINDERS_FETCH_REQUEST';
|
||||
export const BIRTHDAY_REMINDERS_FETCH_SUCCESS = 'BIRTHDAY_REMINDERS_FETCH_SUCCESS';
|
||||
export const BIRTHDAY_REMINDERS_FETCH_FAIL = 'BIRTHDAY_REMINDERS_FETCH_FAIL';
|
||||
|
||||
export function createAccount(params) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: ACCOUNT_CREATE_REQUEST, params });
|
||||
|
@ -1030,3 +1034,26 @@ export function accountLookup(acct, cancelToken) {
|
|||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchBirthdayReminders(day, month) {
|
||||
return (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
const me = getState().get('me');
|
||||
|
||||
dispatch({ type: BIRTHDAY_REMINDERS_FETCH_REQUEST, day, month, id: me });
|
||||
|
||||
api(getState).get('/api/v1/pleroma/birthdays', { params: { day, month } }).then(response => {
|
||||
dispatch(importFetchedAccounts(response.data));
|
||||
dispatch({
|
||||
type: BIRTHDAY_REMINDERS_FETCH_SUCCESS,
|
||||
accounts: response.data,
|
||||
day,
|
||||
month,
|
||||
id: me,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({ type: BIRTHDAY_REMINDERS_FETCH_FAIL, day, month, id: me });
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -100,6 +100,10 @@ export const defaultSettings = ImmutableMap({
|
|||
move: false,
|
||||
'pleroma:emoji_reaction': false,
|
||||
}),
|
||||
|
||||
birthdays: ImmutableMap({
|
||||
show: true,
|
||||
}),
|
||||
}),
|
||||
|
||||
community: ImmutableMap({
|
||||
|
|
130
app/soapbox/components/birthday_input.js
Normal file
130
app/soapbox/components/birthday_input.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const messages = defineMessages({
|
||||
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
|
||||
previousMonth: { id: 'datepicker.previous_month', defaultMessage: 'Previous month' },
|
||||
nextMonth: { id: 'datepicker.next_month', defaultMessage: 'Next month' },
|
||||
previousYear: { id: 'datepicker.previous_year', defaultMessage: 'Previous year' },
|
||||
nextYear: { id: 'datepicker.next_year', defaultMessage: 'Next year' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const features = getFeatures(state.get('instance'));
|
||||
|
||||
return {
|
||||
supportsBirthdays: features.birthdays,
|
||||
minAge: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_min_age']),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class EditProfile extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
hint: PropTypes.node,
|
||||
required: PropTypes.bool,
|
||||
supportsBirthdays: PropTypes.bool,
|
||||
minAge: PropTypes.number,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
value: PropTypes.instanceOf(Date),
|
||||
};
|
||||
|
||||
renderHeader = ({
|
||||
decreaseMonth,
|
||||
increaseMonth,
|
||||
prevMonthButtonDisabled,
|
||||
nextMonthButtonDisabled,
|
||||
decreaseYear,
|
||||
increaseYear,
|
||||
prevYearButtonDisabled,
|
||||
nextYearButtonDisabled,
|
||||
date,
|
||||
}) => {
|
||||
const { intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='datepicker__header'>
|
||||
<div className='datepicker__months'>
|
||||
<IconButton
|
||||
className='datepicker__button'
|
||||
src={require('@tabler/icons/icons/chevron-left.svg')}
|
||||
onClick={decreaseMonth}
|
||||
disabled={prevMonthButtonDisabled}
|
||||
aria-label={intl.formatMessage(messages.previousMonth)}
|
||||
title={intl.formatMessage(messages.previousMonth)}
|
||||
/>
|
||||
{intl.formatDate(date, { month: 'long' })}
|
||||
<IconButton
|
||||
className='datepicker__button'
|
||||
src={require('@tabler/icons/icons/chevron-right.svg')}
|
||||
onClick={increaseMonth}
|
||||
disabled={nextMonthButtonDisabled}
|
||||
aria-label={intl.formatMessage(messages.nextMonth)}
|
||||
title={intl.formatMessage(messages.nextMonth)}
|
||||
/>
|
||||
</div>
|
||||
<div className='datepicker__years'>
|
||||
<IconButton
|
||||
className='datepicker__button'
|
||||
src={require('@tabler/icons/icons/chevron-left.svg')}
|
||||
onClick={decreaseYear}
|
||||
disabled={prevYearButtonDisabled}
|
||||
aria-label={intl.formatMessage(messages.previousYear)}
|
||||
title={intl.formatMessage(messages.previousYear)}
|
||||
/>
|
||||
{intl.formatDate(date, { year: 'numeric' })}
|
||||
<IconButton
|
||||
className='datepicker__button'
|
||||
src={require('@tabler/icons/icons/chevron-right.svg')}
|
||||
onClick={increaseYear}
|
||||
disabled={nextYearButtonDisabled}
|
||||
aria-label={intl.formatMessage(messages.nextYear)}
|
||||
title={intl.formatMessage(messages.nextYear)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, value, onChange, supportsBirthdays, hint, required, minAge } = this.props;
|
||||
|
||||
if (!supportsBirthdays) return null;
|
||||
|
||||
let maxDate = new Date();
|
||||
maxDate = new Date(maxDate.getTime() - minAge * 1000 * 60 * 60 * 24 + maxDate.getTimezoneOffset() * 1000 * 60);
|
||||
|
||||
return (
|
||||
<div className='datepicker'>
|
||||
{hint && (
|
||||
<div className='datepicker__hint'>
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
<div className='datepicker__input'>
|
||||
<DatePicker
|
||||
selected={value}
|
||||
wrapperClassName='react-datepicker-wrapper'
|
||||
onChange={onChange}
|
||||
placeholderText={intl.formatMessage(messages.birthdayPlaceholder)}
|
||||
minDate={new Date('1900-01-01')}
|
||||
maxDate={maxDate}
|
||||
required={required}
|
||||
renderCustomHeader={this.renderHeader}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
154
app/soapbox/components/birthday_reminders.js
Normal file
154
app/soapbox/components/birthday_reminders.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const me = state.get('me');
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const birthdays = state.getIn(['user_lists', 'birthday_reminders', me]);
|
||||
|
||||
if (birthdays && birthdays.size > 0) {
|
||||
return {
|
||||
birthdays,
|
||||
account: getAccount(state, birthdays.first()),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
birthdays,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class BirthdayReminders extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
birthdays: ImmutablePropTypes.orderedSet,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
onMoveDown: PropTypes.func,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
const date = new Date();
|
||||
|
||||
const day = date.getDate();
|
||||
const month = date.getMonth() + 1;
|
||||
|
||||
dispatch(fetchBirthdayReminders(day, month));
|
||||
}
|
||||
|
||||
getHandlers() {
|
||||
return {
|
||||
open: this.handleOpenBirthdaysModal,
|
||||
moveDown: this.props.onMoveDown,
|
||||
};
|
||||
}
|
||||
|
||||
handleOpenBirthdaysModal = () => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(openModal('BIRTHDAYS'));
|
||||
}
|
||||
|
||||
renderMessage() {
|
||||
const { birthdays, account } = this.props;
|
||||
|
||||
const link = (
|
||||
<bdi>
|
||||
<Link
|
||||
className='notification__display-name'
|
||||
title={account.get('acct')}
|
||||
to={`/@${account.get('acct')}`}
|
||||
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
|
||||
/>
|
||||
</bdi>
|
||||
);
|
||||
|
||||
if (birthdays.size === 1) {
|
||||
return <FormattedMessage id='notification.birthday' defaultMessage='{name} has birthday today' values={{ name: link }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='notification.birthday_plural'
|
||||
defaultMessage='{name} and {more} have birthday today'
|
||||
values={{
|
||||
name: link,
|
||||
more: (
|
||||
<span type='button' role='presentation' onClick={this.handleOpenBirthdaysModal}>
|
||||
<FormattedMessage
|
||||
id='notification.birthday.more'
|
||||
defaultMessage='{count} more {count, plural, one {friend} other {friends}}'
|
||||
values={{ count: birthdays.size - 1 }}
|
||||
/>
|
||||
</span>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessageForScreenReader = () => {
|
||||
const { intl, birthdays, account } = this.props;
|
||||
|
||||
if (birthdays.size === 1) {
|
||||
return intl.formatMessage({ id: 'notification.birthday', defaultMessage: '{name} has birthday today' }, { name: account.get('display_name') });
|
||||
}
|
||||
|
||||
return intl.formatMessage(
|
||||
{
|
||||
id: 'notification.birthday_plural',
|
||||
defaultMessage: '{name} and {more} have birthday today',
|
||||
},
|
||||
{
|
||||
name: account.get('display_name'),
|
||||
more: intl.formatMessage(
|
||||
{
|
||||
id: 'notification.birthday.more',
|
||||
defaultMessage: '{count} more {count, plural, one {friend} other {friends}}',
|
||||
},
|
||||
{ count: birthdays.size - 1 },
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { birthdays } = this.props;
|
||||
|
||||
if (!birthdays || birthdays.size === 0) return null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-birthday focusable' tabIndex='0' title={this.renderMessageForScreenReader()}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__icon-wrapper'>
|
||||
<Icon src={require('@tabler/icons/icons/ballon.svg')} />
|
||||
</div>
|
||||
|
||||
<span>
|
||||
{this.renderMessage()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,7 @@ import { accountLookup } from 'soapbox/actions/accounts';
|
|||
import { register, verifyCredentials } from 'soapbox/actions/auth';
|
||||
import { openModal } from 'soapbox/actions/modal';
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import BirthdayInput from 'soapbox/components/birthday_input';
|
||||
import ShowablePassword from 'soapbox/components/showable_password';
|
||||
import CaptchaField from 'soapbox/features/auth_login/components/captcha';
|
||||
import {
|
||||
|
@ -46,6 +47,7 @@ const mapStateToProps = (state, props) => ({
|
|||
needsApproval: state.getIn(['instance', 'approval_required']),
|
||||
supportsEmailList: getFeatures(state.get('instance')).emailList,
|
||||
supportsAccountLookup: getFeatures(state.get('instance')).accountLookup,
|
||||
birthdayRequired: state.getIn(['instance', 'pleroma', 'metadata', 'birthday_required']),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
|
@ -61,6 +63,7 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
supportsEmailList: PropTypes.bool,
|
||||
supportsAccountLookup: PropTypes.bool,
|
||||
inviteToken: PropTypes.string,
|
||||
birthdayRequired: PropTypes.bool,
|
||||
}
|
||||
|
||||
static contextTypes = {
|
||||
|
@ -129,6 +132,12 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
this.setState({ passwordMismatch: !this.passwordsMatch() });
|
||||
}
|
||||
|
||||
onBirthdayChange = birthday => {
|
||||
this.setState({
|
||||
birthday,
|
||||
});
|
||||
}
|
||||
|
||||
launchModal = () => {
|
||||
const { dispatch, intl, needsConfirmation, needsApproval } = this.props;
|
||||
|
||||
|
@ -197,6 +206,7 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
|
||||
onSubmit = e => {
|
||||
const { dispatch, inviteToken } = this.props;
|
||||
const { birthday } = this.state;
|
||||
|
||||
if (!this.passwordsMatch()) {
|
||||
this.setState({ passwordMismatch: true });
|
||||
|
@ -211,6 +221,10 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
if (inviteToken) {
|
||||
params.set('token', inviteToken);
|
||||
}
|
||||
|
||||
if (birthday) {
|
||||
params.set('birthday', new Date(birthday.getTime() - (birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10));
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({ submissionLoading: true });
|
||||
|
@ -245,8 +259,8 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { instance, intl, supportsEmailList } = this.props;
|
||||
const { params, usernameUnavailable, passwordConfirmation, passwordMismatch } = this.state;
|
||||
const { instance, intl, supportsEmailList, birthdayRequired } = this.props;
|
||||
const { params, usernameUnavailable, passwordConfirmation, passwordMismatch, birthday } = this.state;
|
||||
const isLoading = this.state.captchaLoading || this.state.submissionLoading;
|
||||
|
||||
return (
|
||||
|
@ -311,6 +325,12 @@ class RegistrationForm extends ImmutablePureComponent {
|
|||
error={passwordMismatch === true}
|
||||
required
|
||||
/>
|
||||
{birthdayRequired &&
|
||||
<BirthdayInput
|
||||
value={birthday}
|
||||
onChange={this.onBirthdayChange}
|
||||
required
|
||||
/>}
|
||||
{instance.get('approval_required') &&
|
||||
<SimpleTextarea
|
||||
label={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
||||
|
|
88
app/soapbox/features/birthdays/account.js
Normal file
88
app/soapbox/features/birthdays/account.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display_name';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Permalink from 'soapbox/components/permalink';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
birthday: { id: 'account.birthday', defaultMessage: 'Born {date}' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => {
|
||||
const account = getAccount(state, accountId);
|
||||
|
||||
return {
|
||||
account,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps)
|
||||
@injectIntl
|
||||
class Account extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
account: ImmutablePropTypes.map,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
added: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { account, accountId } = this.props;
|
||||
|
||||
if (accountId && !account) {
|
||||
this.props.fetchAccount(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, intl } = this.props;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
const birthday = account.getIn(['pleroma', 'birthday']);
|
||||
if (!birthday) return null;
|
||||
|
||||
const formattedBirthday = intl.formatDate(birthday, { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<Permalink className='account__display-name' title={account.get('acct')} href={`/@${account.get('acct')}`} to={`/@${account.get('acct')}`}>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
|
||||
</div>
|
||||
</Permalink>
|
||||
<div
|
||||
className='account__birthday'
|
||||
title={intl.formatMessage(messages.birthday, {
|
||||
date: formattedBirthday,
|
||||
})}
|
||||
>
|
||||
<Icon src={require('@tabler/icons/icons/ballon.svg')} />
|
||||
{formattedBirthday}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -14,6 +14,7 @@ import { updateNotificationSettings } from 'soapbox/actions/accounts';
|
|||
import { patchMe } from 'soapbox/actions/me';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import BirthdayInput from 'soapbox/components/birthday_input';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import {
|
||||
SimpleForm,
|
||||
|
@ -49,6 +50,7 @@ const messages = defineMessages({
|
|||
error: { id: 'edit_profile.error', defaultMessage: 'Profile update failed' },
|
||||
bioPlaceholder: { id: 'edit_profile.fields.bio_placeholder', defaultMessage: 'Tell us about yourself.' },
|
||||
displayNamePlaceholder: { id: 'edit_profile.fields.display_name_placeholder', defaultMessage: 'Name' },
|
||||
birthdayPlaceholder: { id: 'edit_profile.fields.birthday_placeholder', defaultMessage: 'Your birthday' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
|
@ -58,12 +60,14 @@ const makeMapStateToProps = () => {
|
|||
const me = state.get('me');
|
||||
const account = getAccount(state, me);
|
||||
const soapbox = getSoapboxConfig(state);
|
||||
const features = getFeatures(state.get('instance'));
|
||||
|
||||
return {
|
||||
account,
|
||||
maxFields: state.getIn(['instance', 'pleroma', 'metadata', 'fields_limits', 'max_fields'], 4),
|
||||
verifiedCanEditName: soapbox.get('verifiedCanEditName'),
|
||||
supportsEmailList: getFeatures(state.get('instance')).emailList,
|
||||
supportsEmailList: features.emailList,
|
||||
supportsBirthdays: features.birthdays,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -94,6 +98,8 @@ class EditProfile extends ImmutablePureComponent {
|
|||
account: ImmutablePropTypes.map,
|
||||
maxFields: PropTypes.number,
|
||||
verifiedCanEditName: PropTypes.bool,
|
||||
supportsEmailList: PropTypes.bool,
|
||||
supportsBirthdays: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -107,6 +113,8 @@ class EditProfile extends ImmutablePureComponent {
|
|||
const strangerNotifications = account.getIn(['pleroma', 'notification_settings', 'block_from_strangers']);
|
||||
const acceptsEmailList = account.getIn(['pleroma', 'accepts_email_list']);
|
||||
const discoverable = account.getIn(['source', 'pleroma', 'discoverable']);
|
||||
const birthday = account.getIn(['pleroma', 'birthday']);
|
||||
const showBirthday = account.getIn(['source', 'pleroma', 'show_birthday']);
|
||||
|
||||
const initialState = account.withMutations(map => {
|
||||
map.merge(map.get('source'));
|
||||
|
@ -116,6 +124,11 @@ class EditProfile extends ImmutablePureComponent {
|
|||
map.set('accepts_email_list', acceptsEmailList);
|
||||
map.set('hide_network', hidesNetwork(account));
|
||||
map.set('discoverable', discoverable);
|
||||
map.set('show_birthday', showBirthday);
|
||||
if (birthday) {
|
||||
const date = new Date(birthday);
|
||||
map.set('birthday', new Date(date.getTime() + (date.getTimezoneOffset() * 60000)));
|
||||
}
|
||||
unescapeParams(map, ['display_name', 'bio']);
|
||||
});
|
||||
|
||||
|
@ -156,6 +169,10 @@ class EditProfile extends ImmutablePureComponent {
|
|||
hide_follows: state.hide_network,
|
||||
hide_followers_count: state.hide_network,
|
||||
hide_follows_count: state.hide_network,
|
||||
birthday: state.birthday
|
||||
? new Date(state.birthday.getTime() - (state.birthday.getTimezoneOffset() * 60000)).toISOString().slice(0, 10)
|
||||
: undefined,
|
||||
show_birthday: state.show_birthday,
|
||||
}, this.getFieldParams().toJS());
|
||||
}
|
||||
|
||||
|
@ -223,6 +240,12 @@ class EditProfile extends ImmutablePureComponent {
|
|||
};
|
||||
}
|
||||
|
||||
handleBirthdayChange = birthday => {
|
||||
this.setState({
|
||||
birthday,
|
||||
});
|
||||
}
|
||||
|
||||
handleAddField = () => {
|
||||
this.setState({
|
||||
fields: this.state.fields.push(ImmutableMap({ name: '', value: '' })),
|
||||
|
@ -238,7 +261,7 @@ class EditProfile extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { intl, maxFields, account, verifiedCanEditName, supportsEmailList } = this.props;
|
||||
const { intl, maxFields, account, verifiedCanEditName, supportsBirthdays, supportsEmailList } = this.props;
|
||||
const verified = isVerified(account);
|
||||
const canEditName = verifiedCanEditName || !verified;
|
||||
|
||||
|
@ -267,6 +290,22 @@ class EditProfile extends ImmutablePureComponent {
|
|||
onChange={this.handleTextChange}
|
||||
rows={3}
|
||||
/>
|
||||
{supportsBirthdays && (
|
||||
<>
|
||||
<BirthdayInput
|
||||
hint={<FormattedMessage id='edit_profile.fields.birthday_label' defaultMessage='Birthday' />}
|
||||
value={this.state.birthday}
|
||||
onChange={this.handleBirthdayChange}
|
||||
/>
|
||||
<Checkbox
|
||||
label={<FormattedMessage id='edit_profile.fields.show_birthday_label' defaultMessage='Show my birthday' />}
|
||||
hint={<FormattedMessage id='edit_profile.hints.show_birthday' defaultMessage='Your birthday will be visible on your profile.' />}
|
||||
name='show_birthday'
|
||||
checked={this.state.show_birthday}
|
||||
onChange={this.handleCheckboxChange}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className='fields-row'>
|
||||
<div className='fields-row__column fields-row__column-6'>
|
||||
<ProfilePreview account={this.makePreviewAccount()} />
|
||||
|
|
|
@ -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 = <FormattedMessage id='notifications.column_settings.filter_bar.show' defaultMessage='Show' />;
|
||||
const filterAdvancedStr = <FormattedMessage id='notifications.column_settings.filter_bar.advanced' defaultMessage='Display all categories' />;
|
||||
|
@ -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 && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
const birthdaysStr = <FormattedMessage id='notifications.column_settings.birthdays.show' defaultMessage='Show birthday reminders' />;
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
|
@ -84,6 +86,17 @@ class ColumnSettings extends React.PureComponent {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{supportsBirthdays &&
|
||||
<div role='group' aria-labelledby='notifications-filter-bar'>
|
||||
<span id='notifications-filter-bar' className='column-settings__section'>
|
||||
<FormattedMessage id='notifications.column_settings.birthdays.category' defaultMessage='Birthdays' />
|
||||
</span>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle id='show-filter-bar' prefix='notifications' settings={settings} settingPath={['birthdays', 'show']} onChange={onChange} label={birthdaysStr} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div role='group' aria-labelledby='notifications-follow'>
|
||||
<span id='notifications-follow' className='column-settings__section'><FormattedMessage id='notifications.column_settings.follow' defaultMessage='New followers:' /></span>
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ const mapStateToProps = state => {
|
|||
settings: getSettings(state).get('notifications'),
|
||||
pushSettings: state.get('push_notifications'),
|
||||
supportsEmojiReacts: features.emojiReacts,
|
||||
supportsBirthdays: features.birthdays,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
||||
|
||||
let scrollableContent = null;
|
||||
|
@ -164,6 +188,13 @@ class Notifications extends React.PureComponent {
|
|||
onMoveDown={this.handleMoveDown}
|
||||
/>
|
||||
));
|
||||
|
||||
if (showBirthdayReminders) scrollableContent = scrollableContent.unshift(
|
||||
<BirthdayReminders
|
||||
key='birthdays'
|
||||
onMoveDown={this.handleMoveBelowBirthdays}
|
||||
/>,
|
||||
);
|
||||
} else {
|
||||
scrollableContent = null;
|
||||
}
|
||||
|
|
97
app/soapbox/features/ui/components/birthdays_modal.js
Normal file
97
app/soapbox/features/ui/components/birthdays_modal.js
Normal file
|
@ -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 = <LoadingIndicator />;
|
||||
} else {
|
||||
const emptyMessage = <FormattedMessage id='status.reblogs.empty' defaultMessage='No one has reposted this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
body = (
|
||||
<ScrollableList
|
||||
scrollKey='reblogs'
|
||||
emptyMessage={emptyMessage}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<Account key={id} accountId={id} withNote={false} />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal reactions-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>
|
||||
<FormattedMessage id='column.birthdays' defaultMessage='Birthdays' />
|
||||
</h3>
|
||||
<IconButton
|
||||
className='compose-modal__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
src={require('@tabler/icons/icons/x.svg')}
|
||||
onClick={this.onClickClose} size={20}
|
||||
/>
|
||||
</div>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
<div className='profile-info-panel-content__birthday' title={formattedBirthday}>
|
||||
<Icon src={require('@tabler/icons/icons/ballon.svg')} />
|
||||
<FormattedMessage
|
||||
id='account.birthday_today' defaultMessage='Birthday is today!'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='profile-info-panel-content__birthday'>
|
||||
<Icon src={require('@tabler/icons/icons/ballon.svg')} />
|
||||
<FormattedMessage
|
||||
id='account.birthday' defaultMessage='Born {date}' values={{
|
||||
date: formattedBirthday,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, displayFqn, intl, identity_proofs, username } = this.props;
|
||||
|
||||
|
@ -150,6 +185,8 @@ class ProfileInfoPanel extends ImmutablePureComponent {
|
|||
/>
|
||||
</div>}
|
||||
|
||||
{this.getBirthday()}
|
||||
|
||||
<ProfileStats
|
||||
className='profile-info-panel-content__stats'
|
||||
account={account}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ describe('user_lists reducer', () => {
|
|||
groups: ImmutableMap(),
|
||||
groups_removed_accounts: ImmutableMap(),
|
||||
pinned: ImmutableMap(),
|
||||
birthday_reminders: ImmutableMap(),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ export const getFeatures = createSelector([
|
|||
explicitAddressing: v.software === PLEROMA && gte(v.version, '1.0.0'),
|
||||
accountEndorsements: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||
quotePosts: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||
birthdays: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -554,3 +554,9 @@ a .account__avatar {
|
|||
padding-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.account__birthday {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
|
||||
.datepicker .react-datepicker {
|
||||
box-shadow: 0 0 6px 0 rgb(0 0 0 / 30%);
|
||||
font-family: inherit;
|
||||
font-size: 12px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__join-date {
|
||||
&__join-date,
|
||||
&__birthday {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
color: var(--primary-text-color--faint);
|
||||
|
|
|
@ -634,6 +634,61 @@ code {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.datepicker {
|
||||
padding: 0;
|
||||
margin-bottom: 8px;
|
||||
border: none;
|
||||
|
||||
&__hint {
|
||||
padding-bottom: 0;
|
||||
color: var(--primary-text-color);
|
||||
font-size: 14px;
|
||||
font-style: unset;
|
||||
}
|
||||
|
||||
.react-datepicker {
|
||||
&__header {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
&__input-container {
|
||||
border: 1px solid var(--highlight-text-color);
|
||||
|
||||
input {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__years,
|
||||
&__months {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 0 4px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
width: 28px;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
background: transparent;
|
||||
color: var(--primary-text-color);
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.block-icon {
|
||||
|
|
|
@ -126,7 +126,8 @@ If it's not documented, it's because I inherited it from Mastodon and I don't kn
|
|||
groups: {},
|
||||
followers: {},
|
||||
mutes: {},
|
||||
favourited_by: {}
|
||||
favourited_by: {},
|
||||
birthday_reminders: {}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -391,6 +392,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',
|
||||
|
|
|
@ -118,7 +118,7 @@
|
|||
"qrcode.react": "^1.0.0",
|
||||
"react": "^16.13.1",
|
||||
"react-color": "^2.18.1",
|
||||
"react-datepicker": "^4.1.1",
|
||||
"react-datepicker": "^4.6.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-helmet": "^6.0.0",
|
||||
"react-hotkeys": "^1.1.4",
|
||||
|
|
28
yarn.lock
28
yarn.lock
|
@ -3292,10 +3292,10 @@ data-urls@^2.0.0:
|
|||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
date-fns@^2.0.1:
|
||||
version "2.23.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"
|
||||
integrity sha512-5ycpauovVyAk0kXNZz6ZoB9AYMZB4DObse7P3BPWmyEjXNORTI8EJ6X0uaSAq4sCHzM1uajzrkr6HnsLQpxGXA==
|
||||
date-fns@^2.24.0:
|
||||
version "2.28.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
|
||||
integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
|
||||
|
||||
debug@2.6.9, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
|
@ -7820,16 +7820,16 @@ react-color@^2.18.1:
|
|||
reactcss "^1.2.0"
|
||||
tinycolor2 "^1.4.1"
|
||||
|
||||
react-datepicker@^4.1.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.2.1.tgz#72caf5055bc7c4eb0279c1f6d7624ded053edc4c"
|
||||
integrity sha512-0gcvHMnX8rS1fV90PjjsB7MQdsWNU77JeVHf6bbwK9HnFxgwjVflTx40ebKmHV+leqe+f+FgUP9Nvqbe5RGyfA==
|
||||
react-datepicker@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datepicker/-/react-datepicker-4.6.0.tgz#10fc7c5b9c72df5c3e29712d559cb3fe73fd9f62"
|
||||
integrity sha512-JGSQnQSQYUkS7zvSaZuyHv5lxp3wMrN7GXV0VA0E9Ax9fL3Bb6E1pSXjL6C3WoeuV8dt/mItQfRkPpRGCrl/OA==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.9.2"
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.0.1"
|
||||
date-fns "^2.24.0"
|
||||
prop-types "^15.7.2"
|
||||
react-onclickoutside "^6.10.0"
|
||||
react-onclickoutside "^6.12.0"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
react-dom@^16.13.1:
|
||||
|
@ -7959,10 +7959,10 @@ react-notification@^6.8.4:
|
|||
dependencies:
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-onclickoutside@^6.10.0:
|
||||
version "6.12.0"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.0.tgz#c63db2e3c2c852b288160cdb6cff443604e28db4"
|
||||
integrity sha512-oPlOTYcISLHfpMog2lUZMFSbqOs4LFcA4+vo7fpfevB5v9Z0D5VBDBkfeO5lv+hpEcGoaGk67braLT+QT+eICA==
|
||||
react-onclickoutside@^6.12.0:
|
||||
version "6.12.1"
|
||||
resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.12.1.tgz#92dddd28f55e483a1838c5c2930e051168c1e96b"
|
||||
integrity sha512-a5Q7CkWznBRUWPmocCvE8b6lEYw1s6+opp/60dCunhO+G6E4tDTO2Sd2jKE+leEnnrLAE2Wj5DlDHNqj5wPv1Q==
|
||||
|
||||
react-overlays@^0.9.0:
|
||||
version "0.9.3"
|
||||
|
|
Loading…
Reference in a new issue