diff --git a/.eslintrc.js b/.eslintrc.js
index d885cbeea..ae8c9e981 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -150,7 +150,6 @@ module.exports = {
'react/jsx-wrap-multilines': 'error',
'react/no-multi-comp': 'off',
'react/no-string-refs': 'error',
- 'react/prop-types': 'error',
'react/self-closing-comp': 'error',
'jsx-a11y/accessible-emoji': 'warn',
@@ -264,7 +263,6 @@ module.exports = {
files: ['**/*.ts', '**/*.tsx'],
rules: {
'no-undef': 'off', // https://stackoverflow.com/a/69155899
- 'react/prop-types': 'off',
},
parser: '@typescript-eslint/parser',
},
diff --git a/app/soapbox/actions/accounts.js b/app/soapbox/actions/accounts.js
index 23aad5601..1005af838 100644
--- a/app/soapbox/actions/accounts.js
+++ b/app/soapbox/actions/accounts.js
@@ -1035,7 +1035,7 @@ export function accountLookup(acct, cancelToken) {
};
}
-export function fetchBirthdayReminders(day, month) {
+export function fetchBirthdayReminders(month, day) {
return (dispatch, getState) => {
if (!isLoggedIn(getState)) return;
diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx
index ab9c7af68..c60893e5a 100644
--- a/app/soapbox/components/account.tsx
+++ b/app/soapbox/components/account.tsx
@@ -5,7 +5,7 @@ import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
import VerificationBadge from 'soapbox/components/verification_badge';
import ActionButton from 'soapbox/features/ui/components/action_button';
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
-import { getAcct, getDomain } from 'soapbox/utils/accounts';
+import { getAcct } from 'soapbox/utils/accounts';
import { displayFqn } from 'soapbox/utils/state';
import RelativeTimestamp from './relative_timestamp';
@@ -91,7 +91,7 @@ const Account = ({
);
}
- if (account.get('id') !== me && account.get('relationship', null) !== null) {
+ if (account.id !== me) {
return ;
}
@@ -118,8 +118,8 @@ const Account = ({
if (hidden) {
return (
<>
- {account.get('display_name')}
- {account.get('username')}
+ {account.display_name}
+ {account.username}
>
);
}
@@ -128,34 +128,31 @@ const Account = ({
const LinkEl: any = showProfileHoverCard ? Link : 'div';
- const favicon = account.pleroma.get('favicon');
- const domain = getDomain(account);
-
return (
{children}}
+ wrapper={(children) => {children}}
>
event.stopPropagation()}
>
-
+
{children}}
+ wrapper={(children) => {children}}
>
event.stopPropagation()}
>
@@ -163,10 +160,10 @@ const Account = ({
size='sm'
weight='semibold'
truncate
- dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
+ dangerouslySetInnerHTML={{ __html: account.display_name_html }}
/>
- {account.get('verified') && }
+ {account.verified && }
@@ -174,9 +171,9 @@ const Account = ({
@{username}
- {favicon && (
-
-
+ {account.favicon && (
+ e.stopPropagation()}>
+
)}
diff --git a/app/soapbox/components/birthday-panel.tsx b/app/soapbox/components/birthday-panel.tsx
new file mode 100644
index 000000000..6008cb423
--- /dev/null
+++ b/app/soapbox/components/birthday-panel.tsx
@@ -0,0 +1,47 @@
+import { OrderedSet as ImmutableOrderedSet } from 'immutable';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { useDispatch } from 'react-redux';
+
+import { fetchBirthdayReminders } from 'soapbox/actions/accounts';
+import { Widget } from 'soapbox/components/ui';
+import AccountContainer from 'soapbox/containers/account_container';
+import { useAppSelector } from 'soapbox/hooks';
+
+interface IBirthdayPanel {
+ limit: number
+}
+
+const BirthdayPanel = ({ limit }: IBirthdayPanel) => {
+ const dispatch = useDispatch();
+
+ const birthdays: ImmutableOrderedSet = useAppSelector(state => state.user_lists.getIn(['birthday_reminders', state.me], ImmutableOrderedSet()));
+ const birthdaysToRender = birthdays.slice(0, limit);
+
+ React.useEffect(() => {
+ const date = new Date();
+
+ const day = date.getDate();
+ const month = date.getMonth() + 1;
+
+ dispatch(fetchBirthdayReminders(month, day));
+ }, []);
+
+ if (birthdaysToRender.isEmpty()) {
+ return null;
+ }
+
+ return (
+ }>
+ {birthdaysToRender.map(accountId => (
+ , but it isn't
+ id={accountId}
+ />
+ ))}
+
+ );
+};
+
+export default BirthdayPanel;
diff --git a/app/soapbox/components/birthday_reminders.js b/app/soapbox/components/birthday_reminders.js
deleted file mode 100644
index d57452150..000000000
--- a/app/soapbox/components/birthday_reminders.js
+++ /dev/null
@@ -1,161 +0,0 @@
-
-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/modals';
-import Icon from 'soapbox/components/icon';
-import { HStack, Text } from 'soapbox/components/ui';
-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?.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 = (
-
-
-
- );
-
- if (birthdays.size === 1) {
- return ;
- }
-
- return (
-
-
-
- ),
- }}
- />
- );
- }
-
- renderMessageForScreenReader = () => {
- const { intl, birthdays, account } = this.props;
-
- if (birthdays.size === 1) {
- return intl.formatMessage({ id: 'notification.birthday', defaultMessage: '{name} has a 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 (
-
-
-
-
-
-
-
- {this.renderMessage()}
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx
index e5cc6dfd1..1257d9779 100644
--- a/app/soapbox/components/ui/widget/widget.tsx
+++ b/app/soapbox/components/ui/widget/widget.tsx
@@ -1,9 +1,11 @@
import React from 'react';
-import { Stack, Text } from 'soapbox/components/ui';
+import { Text, IconButton } from 'soapbox/components/ui';
+import HStack from 'soapbox/components/ui/hstack/hstack';
+import Stack from 'soapbox/components/ui/stack/stack';
interface IWidgetTitle {
- title: string | React.ReactNode
+ title: string | React.ReactNode,
}
const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
@@ -16,12 +18,31 @@ const WidgetBody: React.FC = ({ children }): JSX.Element => (
interface IWidget {
title: string | React.ReactNode,
+ onActionClick?: () => void,
+ actionIcon?: string,
+ actionTitle?: string,
}
-const Widget: React.FC = ({ title, children }): JSX.Element => {
+const Widget: React.FC = ({
+ title,
+ children,
+ onActionClick,
+ actionIcon = require('@tabler/icons/icons/arrow-right.svg'),
+ actionTitle,
+}): JSX.Element => {
return (
-
+
+
+ {onActionClick && (
+
+ )}
+
{children}
);
diff --git a/app/soapbox/features/chats/components/chat_list.tsx b/app/soapbox/features/chats/components/chat_list.tsx
index 2608a8f03..8347c0e89 100644
--- a/app/soapbox/features/chats/components/chat_list.tsx
+++ b/app/soapbox/features/chats/components/chat_list.tsx
@@ -78,9 +78,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false })
Footer: () => hasMore ? : null,
EmptyPlaceholder: () => {
if (isLoading) {
- return (
- <>{Array(20).fill()}>
- );
+ return ;
} else {
return {intl.formatMessage(messages.emptyMessage)};
}
diff --git a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.tsx b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.tsx
index 073852a8b..581f53f03 100644
--- a/app/soapbox/features/crypto_donate/components/crypto_donate_panel.tsx
+++ b/app/soapbox/features/crypto_donate/components/crypto_donate_panel.tsx
@@ -1,17 +1,24 @@
import React from 'react';
-import { FormattedMessage } from 'react-intl';
-import { Link } from 'react-router-dom';
+import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
+import { useHistory } from 'react-router-dom';
import { Text, Widget } from 'soapbox/components/ui';
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
import SiteWallet from './site_wallet';
+const messages = defineMessages({
+ actionTitle: { id: 'crypto_donate_panel.actions.view', defaultMessage: 'Click to see {count} {count, plural, one {wallet} other {wallets}}' },
+});
+
interface ICryptoDonatePanel {
limit: number,
}
const CryptoDonatePanel: React.FC = ({ limit = 3 }): JSX.Element | null => {
+ const intl = useIntl();
+ const history = useHistory();
+
const addresses = useSoapboxConfig().get('cryptoAddresses');
const siteTitle = useAppSelector((state) => state.instance.title);
@@ -19,11 +26,16 @@ const CryptoDonatePanel: React.FC = ({ limit = 3 }): JSX.Ele
return null;
}
- const more = addresses.size - limit;
- const hasMore = more > 0;
+ const handleAction = () => {
+ history.push('/donate/crypto');
+ };
return (
- }>
+ }
+ onActionClick={handleAction}
+ actionTitle={intl.formatMessage(messages.actionTitle, { count: addresses.size })}
+ >
= ({ limit = 3 }): JSX.Ele
-
- {hasMore && (
-
-
-
-
-
- )}
);
};
diff --git a/app/soapbox/features/edit_profile/index.js b/app/soapbox/features/edit_profile/index.js
index b683e8f19..fe0c66882 100644
--- a/app/soapbox/features/edit_profile/index.js
+++ b/app/soapbox/features/edit_profile/index.js
@@ -150,6 +150,7 @@ class EditProfile extends ImmutablePureComponent {
display_name: state.display_name,
website: state.website,
location: state.location,
+ birthday: state.birthday,
note: state.note,
avatar: state.avatar_file,
header: state.header_file,
@@ -263,6 +264,18 @@ class EditProfile extends ImmutablePureComponent {
/>
+ {features.birthdays && (
+ }
+ >
+
+
+ )}
+
{features.accountLocation && (
}
diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js
index fce474a96..2ed33a9c5 100644
--- a/app/soapbox/features/notifications/index.js
+++ b/app/soapbox/features/notifications/index.js
@@ -1,4 +1,3 @@
-import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
@@ -6,22 +5,20 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
+import { Virtuoso } from 'react-virtuoso';
import { createSelector } from 'reselect';
import { getSettings } from 'soapbox/actions/settings';
-import BirthdayReminders from 'soapbox/components/birthday_reminders';
+import PullToRefresh from 'soapbox/components/pull-to-refresh';
import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
-import { getFeatures } from 'soapbox/utils/features';
import {
expandNotifications,
scrollTopNotifications,
dequeueNotifications,
} from '../../actions/notifications';
-import LoadGap from '../../components/load_gap';
-import ScrollableList from '../../components/scrollable_list';
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
-import { Column } from '../../components/ui';
+import { Column, Text } from '../../components/ui';
import FilterBarContainer from './containers/filter_bar_container';
import NotificationContainer from './containers/notification_container';
@@ -31,6 +28,24 @@ const messages = defineMessages({
queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' },
});
+const Footer = ({ context }) => (
+ context.hasMore ? (
+
+ ) : null
+);
+
+const EmptyPlaceholder = ({ context }) => {
+ if (context.isLoading) {
+ return ;
+ } else {
+ return {context.emptyMessage};
+ }
+};
+
+const Item = ({ context, ...rest }) => (
+
+);
+
const getNotifications = createSelector([
state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']),
@@ -48,10 +63,6 @@ const getNotifications = createSelector([
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']),
@@ -60,8 +71,6 @@ const mapStateToProps = state => {
isUnread: state.getIn(['notifications', 'unread']) > 0,
hasMore: state.getIn(['notifications', 'hasMore']),
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
- showBirthdayReminders,
- hasBirthdays: !!birthdays,
};
};
@@ -79,8 +88,6 @@ class Notifications extends React.PureComponent {
hasMore: PropTypes.bool,
dequeueNotifications: PropTypes.func,
totalQueuedNotificationsCount: PropTypes.number,
- showBirthdayReminders: PropTypes.bool,
- hasBirthdays: PropTypes.bool,
};
componentWillUnmount() {
@@ -117,25 +124,15 @@ class Notifications extends React.PureComponent {
}
handleMoveUp = id => {
- const { hasBirthdays } = this.props;
-
- let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
- if (hasBirthdays) elementIndex++;
+ const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
this._selectChild(elementIndex, true);
}
handleMoveDown = id => {
- const { hasBirthdays } = this.props;
-
- let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
- if (hasBirthdays) elementIndex++;
+ const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
this._selectChild(elementIndex, false);
}
- handleMoveBelowBirthdays = () => {
- this._selectChild(1, false);
- }
-
_selectChild(index, align_top) {
const container = this.column;
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
@@ -160,66 +157,42 @@ class Notifications extends React.PureComponent {
}
render() {
- const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount, showBirthdayReminders } = this.props;
+ const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
const emptyMessage = ;
- let scrollableContent = null;
-
const filterBarContainer = showFilterBar
? ()
: null;
- if (isLoading && this.scrollableContent) {
- scrollableContent = this.scrollableContent;
- } else if (notifications.size > 0 || hasMore) {
- scrollableContent = notifications.map((item, index) => item === null ? (
- 0 ? notifications.getIn([index - 1, 'id']) : null}
- onClick={this.handleLoadGap}
+ const scrollContainer = (
+
+ isScrolling && this.handleScroll}
+ itemContent={(_index, notification) => (
+
+ )}
+ context={{
+ hasMore,
+ isLoading,
+ emptyMessage,
+ }}
+ components={{
+ ScrollSeekPlaceholder: PlaceholderNotification,
+ Footer,
+ EmptyPlaceholder,
+ Item,
+ }}
/>
- ) : (
-
- ));
-
- if (showBirthdayReminders) scrollableContent = scrollableContent.unshift(
- ,
- );
- } else {
- scrollableContent = null;
- }
-
- this.scrollableContent = scrollableContent;
-
- const scrollContainer = (
- 0,
- 'space-y-2': notifications.size === 0,
- })}
- >
- {scrollableContent}
-
+
);
return (
diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx
index cea616407..25a730603 100644
--- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx
+++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx
@@ -36,8 +36,16 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
return null;
}
+ // FIXME: This page actually doesn't look good right now
+ // const handleAction = () => {
+ // history.push('/suggestions');
+ // };
+
return (
- }>
+ }
+ // onAction={handleAction}
+ >
{suggestionsToRender.map((suggestion: ImmutableMap) => (
{
expect(result.staff).toBe(true);
expect(result.moderator).toBe(false);
});
+
+ it('normalizes Pleroma favicon', () => {
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
+ const result = normalizeAccount(account);
+
+ expect(result.favicon).toEqual('https://gleasonator.com/favicon.png');
+ });
+
+ it('adds account domain', () => {
+ const account = require('soapbox/__fixtures__/pleroma-account.json');
+ const result = normalizeAccount(account);
+
+ expect(result.domain).toEqual('gleasonator.com');
+ });
});
diff --git a/app/soapbox/normalizers/account.ts b/app/soapbox/normalizers/account.ts
index 679dd770d..2ca866c18 100644
--- a/app/soapbox/normalizers/account.ts
+++ b/app/soapbox/normalizers/account.ts
@@ -28,6 +28,7 @@ export const AccountRecord = ImmutableRecord({
created_at: new Date(),
display_name: '',
emojis: ImmutableList(),
+ favicon: '',
fields: ImmutableList(),
followers_count: 0,
following_count: 0,
@@ -52,6 +53,7 @@ export const AccountRecord = ImmutableRecord({
// Internal fields
admin: false,
display_name_html: '',
+ domain: '',
moderator: false,
note_emojified: '',
note_plain: '',
@@ -224,6 +226,16 @@ const normalizeFqn = (account: ImmutableMap) => {
return account.set('fqn', fqn);
};
+const normalizeFavicon = (account: ImmutableMap) => {
+ const favicon = account.getIn(['pleroma', 'favicon']) || '';
+ return account.set('favicon', favicon);
+};
+
+const addDomain = (account: ImmutableMap) => {
+ const domain = account.get('fqn', '').split('@')[1] || '';
+ return account.set('domain', domain);
+};
+
const addStaffFields = (account: ImmutableMap) => {
const admin = account.getIn(['pleroma', 'is_admin']) === true;
const moderator = account.getIn(['pleroma', 'is_moderator']) === true;
@@ -248,6 +260,8 @@ export const normalizeAccount = (account: Record) => {
normalizeBirthday(account);
normalizeLocation(account);
normalizeFqn(account);
+ normalizeFavicon(account);
+ addDomain(account);
addStaffFields(account);
fixUsername(account);
fixDisplayName(account);
diff --git a/app/soapbox/pages/home_page.js b/app/soapbox/pages/home_page.js
index e8a12a5d8..e9b6630aa 100644
--- a/app/soapbox/pages/home_page.js
+++ b/app/soapbox/pages/home_page.js
@@ -11,6 +11,7 @@ import {
TrendsPanel,
SignUpPanel,
CryptoDonatePanel,
+ BirthdayPanel,
} from 'soapbox/features/ui/util/async-components';
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
import { getFeatures } from 'soapbox/utils/features';
@@ -93,6 +94,11 @@ class HomePage extends ImmutablePureComponent {
{Component => }
)}
+ {features.birthdays && (
+
+ {Component => }
+
+ )}
{features.suggestions && (
{Component => }