diff --git a/app/images/soapbox-logo.svg b/app/images/soapbox-logo.svg new file mode 100644 index 000000000..270b7b810 --- /dev/null +++ b/app/images/soapbox-logo.svg @@ -0,0 +1 @@ + diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js index aacdcd106..23aad5601 100644 --- a/app/soapbox/actions/accounts.js +++ b/app/soapbox/actions/accounts.js @@ -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 }); @@ -156,14 +160,7 @@ export function fetchAccountByUsername(username) { const features = getFeatures(instance); const me = state.get('me'); - if (!me && features.accountLookup) { - dispatch(accountLookup(username)).then(account => { - dispatch(fetchAccountSuccess(account)); - }).catch(error => { - dispatch(fetchAccountFail(null, error)); - dispatch(importErrorWhileFetchingAccountByUsername(username)); - }); - } else if (features.accountByUsername) { + if (features.accountByUsername && (me || !features.accountLookup)) { api(getState).get(`/api/v1/accounts/${username}`).then(response => { dispatch(fetchRelationships([response.data.id])); dispatch(importFetchedAccount(response.data)); @@ -172,6 +169,13 @@ export function fetchAccountByUsername(username) { dispatch(fetchAccountFail(null, error)); dispatch(importErrorWhileFetchingAccountByUsername(username)); }); + } else if (features.accountLookup) { + dispatch(accountLookup(username)).then(account => { + dispatch(fetchAccountSuccess(account)); + }).catch(error => { + dispatch(fetchAccountFail(null, error)); + dispatch(importErrorWhileFetchingAccountByUsername(username)); + }); } else { dispatch(accountSearch({ q: username, @@ -217,7 +221,7 @@ export function fetchAccountFail(id, error) { }; } -export function followAccount(id, reblogs = true) { +export function followAccount(id, options = { reblogs: true }) { return (dispatch, getState) => { if (!isLoggedIn(getState)) return; @@ -226,7 +230,7 @@ export function followAccount(id, reblogs = true) { dispatch(followAccountRequest(id, locked)); - api(getState).post(`/api/v1/accounts/${id}/follow`, { reblogs }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/follow`, options).then(response => { dispatch(followAccountSuccess(response.data, alreadyFollowing)); }).catch(error => { dispatch(followAccountFail(error, locked)); @@ -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 }); + }); + }; +} diff --git a/app/soapbox/actions/settings.js b/app/soapbox/actions/settings.js index 4a50909fa..418c41292 100644 --- a/app/soapbox/actions/settings.js +++ b/app/soapbox/actions/settings.js @@ -100,6 +100,10 @@ export const defaultSettings = ImmutableMap({ move: false, 'pleroma:emoji_reaction': false, }), + + birthdays: ImmutableMap({ + show: true, + }), }), community: ImmutableMap({ diff --git a/app/soapbox/components/birthday_input.js b/app/soapbox/components/birthday_input.js new file mode 100644 index 000000000..12f42bb59 --- /dev/null +++ b/app/soapbox/components/birthday_input.js @@ -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 ( +
/g, '\n\n').replace(/<[^>]*>/g, ''));
+ unescape(html.replace(/
/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, ''));
const handlePush = (event) => {
const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json();
diff --git a/app/soapbox/utils/features.js b/app/soapbox/utils/features.js
index fc3dc68ab..1b3ea5369 100644
--- a/app/soapbox/utils/features.js
+++ b/app/soapbox/utils/features.js
@@ -65,6 +65,10 @@ export const getFeatures = createSelector([
resetPasswordAPI: v.software === PLEROMA,
exposableReactions: features.includes('exposable_reactions'),
accountSubscriptions: v.software === PLEROMA && gte(v.version, '1.0.0'),
+ accountNotifies: any([
+ v.software === MASTODON && gte(v.compatVersion, '3.3.0'),
+ v.software === PLEROMA && gte(v.version, '2.4.50'),
+ ]),
unrestrictedLists: v.software === PLEROMA,
accountByUsername: v.software === PLEROMA,
profileDirectory: any([
@@ -79,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'),
};
});
diff --git a/app/soapbox/utils/html.js b/app/soapbox/utils/html.js
index fec437c03..9eddcf876 100644
--- a/app/soapbox/utils/html.js
+++ b/app/soapbox/utils/html.js
@@ -1,7 +1,7 @@
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html) => {
const wrapper = document.createElement('div');
- wrapper.innerHTML = html.replace(/
/g, '\n').replace(/<\/p>
/g, '\n\n').replace(/<[^>]*>/g, '');
+ wrapper.innerHTML = html.replace(/
/g, '\n').replace(/<\/p><[^>]*>/g, '\n\n').replace(/<[^>]*>/g, '');
return wrapper.textContent;
};
@@ -14,12 +14,11 @@ export const stripCompatibilityFeatures = html => {
'.recipients-inline',
];
+ // Remove all instances of all selectors
selectors.forEach(selector => {
- const elem = node.querySelector(selector);
-
- if (elem) {
+ node.querySelectorAll(selector).forEach(elem => {
elem.remove();
- }
+ });
});
return node.innerHTML;
diff --git a/app/styles/accounts.scss b/app/styles/accounts.scss
index 7b0a53fe9..340680f10 100644
--- a/app/styles/accounts.scss
+++ b/app/styles/accounts.scss
@@ -554,3 +554,9 @@ a .account__avatar {
padding-right: 3px;
}
}
+
+.account__birthday {
+ display: flex;
+ align-items: center;
+ white-space: nowrap;
+}
diff --git a/app/styles/components/datepicker.scss b/app/styles/components/datepicker.scss
index 76996ffda..dd74db173 100644
--- a/app/styles/components/datepicker.scss
+++ b/app/styles/components/datepicker.scss
@@ -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;
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/app/styles/components/profile-info-panel.scss b/app/styles/components/profile-info-panel.scss
index 99bf66c09..a3c3f6dc5 100644
--- a/app/styles/components/profile-info-panel.scss
+++ b/app/styles/components/profile-info-panel.scss
@@ -22,7 +22,8 @@
flex-wrap: wrap;
}
- &__join-date {
+ &__join-date,
+ &__birthday {
display: flex;
font-size: 14px;
color: var(--primary-text-color--faint);
diff --git a/app/styles/components/status.scss b/app/styles/components/status.scss
index 9633ae100..ba06c5039 100644
--- a/app/styles/components/status.scss
+++ b/app/styles/components/status.scss
@@ -804,6 +804,12 @@ a.status-card.compact:hover {
}
&__content {
+ display: -webkit-box;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ -webkit-line-clamp: 6;
+ -webkit-box-orient: vertical;
+ max-height: 114px;
margin-top: 5px;
font-size: 14px;
@@ -820,4 +826,14 @@ a.status-card.compact:hover {
.attachment-thumbs .media-gallery {
margin-top: 5px !important;
}
+
+ &-tombstone {
+ margin-top: 14px;
+ padding: 12px;
+ border: 1px solid var(--brand-color--med);
+ border-radius: 10px;
+ color: var(--primary-text-color--faint);
+ font-size: 14px;
+ text-align: center;
+ }
}
diff --git a/app/styles/forms.scss b/app/styles/forms.scss
index 9fc4ad170..64ab2fb4d 100644
--- a/app/styles/forms.scss
+++ b/app/styles/forms.scss
@@ -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 {
diff --git a/docs/store.md b/docs/store.md
index 34088ab8c..7c0bff506 100644
--- a/docs/store.md
+++ b/docs/store.md
@@ -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',
diff --git a/package.json b/package.json
index 72ad7dfb5..2706765e6 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/yarn.lock b/yarn.lock
index 48f6a651c..53ae2f599 100644
--- a/yarn.lock
+++ b/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"