Merge branch 'virtuoso-notifications' into 'next'
Next: Notifications improvements 2 See merge request soapbox-pub/soapbox-fe!1242
This commit is contained in:
commit
55c4e3a00b
|
@ -150,7 +150,6 @@ module.exports = {
|
||||||
'react/jsx-wrap-multilines': 'error',
|
'react/jsx-wrap-multilines': 'error',
|
||||||
'react/no-multi-comp': 'off',
|
'react/no-multi-comp': 'off',
|
||||||
'react/no-string-refs': 'error',
|
'react/no-string-refs': 'error',
|
||||||
'react/prop-types': 'error',
|
|
||||||
'react/self-closing-comp': 'error',
|
'react/self-closing-comp': 'error',
|
||||||
|
|
||||||
'jsx-a11y/accessible-emoji': 'warn',
|
'jsx-a11y/accessible-emoji': 'warn',
|
||||||
|
@ -264,7 +263,6 @@ module.exports = {
|
||||||
files: ['**/*.ts', '**/*.tsx'],
|
files: ['**/*.ts', '**/*.tsx'],
|
||||||
rules: {
|
rules: {
|
||||||
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
'no-undef': 'off', // https://stackoverflow.com/a/69155899
|
||||||
'react/prop-types': 'off',
|
|
||||||
},
|
},
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1035,7 +1035,7 @@ export function accountLookup(acct, cancelToken) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchBirthdayReminders(day, month) {
|
export function fetchBirthdayReminders(month, day) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
||||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
import ActionButton from 'soapbox/features/ui/components/action_button';
|
import ActionButton from 'soapbox/features/ui/components/action_button';
|
||||||
import { useAppSelector, useOnScreen } from 'soapbox/hooks';
|
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 { displayFqn } from 'soapbox/utils/state';
|
||||||
|
|
||||||
import RelativeTimestamp from './relative_timestamp';
|
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 <ActionButton account={account} />;
|
return <ActionButton account={account} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,8 +118,8 @@ const Account = ({
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{account.get('display_name')}
|
{account.display_name}
|
||||||
{account.get('username')}
|
{account.username}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -128,34 +128,31 @@ const Account = ({
|
||||||
|
|
||||||
const LinkEl: any = showProfileHoverCard ? Link : 'div';
|
const LinkEl: any = showProfileHoverCard ? Link : 'div';
|
||||||
|
|
||||||
const favicon = account.pleroma.get('favicon');
|
|
||||||
const domain = getDomain(account);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
||||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||||
<HStack alignItems='center' space={3} grow>
|
<HStack alignItems='center' space={3} grow>
|
||||||
<ProfilePopper
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
wrapper={(children) => <HoverRefWrapper accountId={account.get('id')} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
>
|
>
|
||||||
<LinkEl
|
<LinkEl
|
||||||
to={`/@${account.get('acct')}`}
|
to={`/@${account.acct}`}
|
||||||
title={account.get('acct')}
|
title={account.acct}
|
||||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<Avatar src={account.get('avatar')} size={avatarSize} />
|
<Avatar src={account.avatar} size={avatarSize} />
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
</ProfilePopper>
|
</ProfilePopper>
|
||||||
|
|
||||||
<div className='flex-grow'>
|
<div className='flex-grow'>
|
||||||
<ProfilePopper
|
<ProfilePopper
|
||||||
condition={showProfileHoverCard}
|
condition={showProfileHoverCard}
|
||||||
wrapper={(children) => <HoverRefWrapper accountId={account.get('id')} inline>{children}</HoverRefWrapper>}
|
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||||
>
|
>
|
||||||
<LinkEl
|
<LinkEl
|
||||||
to={`/@${account.get('acct')}`}
|
to={`/@${account.acct}`}
|
||||||
title={account.get('acct')}
|
title={account.acct}
|
||||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className='flex items-center space-x-1 flex-grow' style={style}>
|
<div className='flex items-center space-x-1 flex-grow' style={style}>
|
||||||
|
@ -163,10 +160,10 @@ const Account = ({
|
||||||
size='sm'
|
size='sm'
|
||||||
weight='semibold'
|
weight='semibold'
|
||||||
truncate
|
truncate
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
|
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{account.get('verified') && <VerificationBadge />}
|
{account.verified && <VerificationBadge />}
|
||||||
</div>
|
</div>
|
||||||
</LinkEl>
|
</LinkEl>
|
||||||
</ProfilePopper>
|
</ProfilePopper>
|
||||||
|
@ -174,9 +171,9 @@ const Account = ({
|
||||||
<HStack alignItems='center' space={1} style={style}>
|
<HStack alignItems='center' space={1} style={style}>
|
||||||
<Text theme='muted' size='sm' truncate>@{username}</Text>
|
<Text theme='muted' size='sm' truncate>@{username}</Text>
|
||||||
|
|
||||||
{favicon && (
|
{account.favicon && (
|
||||||
<Link to={`/timeline/${domain}`} className='w-4 h-4'>
|
<Link to={`/timeline/${account.domain}`} className='w-4 h-4 flex-none' onClick={e => e.stopPropagation()}>
|
||||||
<img src={favicon} alt='' title={domain} className='w-full max-h-full' />
|
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -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<string> = 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 (
|
||||||
|
<Widget title={<FormattedMessage id='birthday_panel.title' defaultMessage='Birthdays' />}>
|
||||||
|
{birthdaysToRender.map(accountId => (
|
||||||
|
<AccountContainer
|
||||||
|
key={accountId}
|
||||||
|
// @ts-ignore: TS thinks `id` is passed to <Account>, but it isn't
|
||||||
|
id={accountId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Widget>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BirthdayPanel;
|
|
@ -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 = (
|
|
||||||
<bdi>
|
|
||||||
<Link
|
|
||||||
className='text-gray-800 dark:text-gray-200 font-bold hover:underline'
|
|
||||||
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 a 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 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 (
|
|
||||||
<HotKeys handlers={this.getHandlers()}>
|
|
||||||
<div className='notification notification-birthday focusable' tabIndex='0' title={this.renderMessageForScreenReader()}>
|
|
||||||
<div className='p-4 focusable'>
|
|
||||||
<HStack alignItems='center' space={1.5}>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/icons/ballon.svg')}
|
|
||||||
className='text-primary-600'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text
|
|
||||||
theme='muted'
|
|
||||||
size='sm'
|
|
||||||
>
|
|
||||||
{this.renderMessage()}
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</HotKeys>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,9 +1,11 @@
|
||||||
import React from 'react';
|
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 {
|
interface IWidgetTitle {
|
||||||
title: string | React.ReactNode
|
title: string | React.ReactNode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
|
const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
|
||||||
|
@ -16,12 +18,31 @@ const WidgetBody: React.FC = ({ children }): JSX.Element => (
|
||||||
|
|
||||||
interface IWidget {
|
interface IWidget {
|
||||||
title: string | React.ReactNode,
|
title: string | React.ReactNode,
|
||||||
|
onActionClick?: () => void,
|
||||||
|
actionIcon?: string,
|
||||||
|
actionTitle?: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Widget: React.FC<IWidget> = ({ title, children }): JSX.Element => {
|
const Widget: React.FC<IWidget> = ({
|
||||||
|
title,
|
||||||
|
children,
|
||||||
|
onActionClick,
|
||||||
|
actionIcon = require('@tabler/icons/icons/arrow-right.svg'),
|
||||||
|
actionTitle,
|
||||||
|
}): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
<WidgetTitle title={title} />
|
<HStack alignItems='center'>
|
||||||
|
<WidgetTitle title={title} />
|
||||||
|
{onActionClick && (
|
||||||
|
<IconButton
|
||||||
|
className='w-6 h-6 ml-2 text-black dark:text-white'
|
||||||
|
src={actionIcon}
|
||||||
|
onClick={onActionClick}
|
||||||
|
title={actionTitle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
<WidgetBody>{children}</WidgetBody>
|
<WidgetBody>{children}</WidgetBody>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -78,9 +78,7 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false })
|
||||||
Footer: () => hasMore ? <PlaceholderChat /> : null,
|
Footer: () => hasMore ? <PlaceholderChat /> : null,
|
||||||
EmptyPlaceholder: () => {
|
EmptyPlaceholder: () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return <PlaceholderChat />;
|
||||||
<>{Array(20).fill(<PlaceholderChat />)}</>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
return <Text>{intl.formatMessage(messages.emptyMessage)}</Text>;
|
return <Text>{intl.formatMessage(messages.emptyMessage)}</Text>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import { Text, Widget } from 'soapbox/components/ui';
|
import { Text, Widget } from 'soapbox/components/ui';
|
||||||
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppSelector, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
|
||||||
import SiteWallet from './site_wallet';
|
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 {
|
interface ICryptoDonatePanel {
|
||||||
limit: number,
|
limit: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Element | null => {
|
const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Element | null => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const addresses = useSoapboxConfig().get('cryptoAddresses');
|
const addresses = useSoapboxConfig().get('cryptoAddresses');
|
||||||
const siteTitle = useAppSelector((state) => state.instance.title);
|
const siteTitle = useAppSelector((state) => state.instance.title);
|
||||||
|
|
||||||
|
@ -19,11 +26,16 @@ const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Ele
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const more = addresses.size - limit;
|
const handleAction = () => {
|
||||||
const hasMore = more > 0;
|
history.push('/donate/crypto');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget title={<FormattedMessage id='crypto_donate_panel.heading' defaultMessage='Donate Cryptocurrency' />}>
|
<Widget
|
||||||
|
title={<FormattedMessage id='crypto_donate_panel.heading' defaultMessage='Donate Cryptocurrency' />}
|
||||||
|
onActionClick={handleAction}
|
||||||
|
actionTitle={intl.formatMessage(messages.actionTitle, { count: addresses.size })}
|
||||||
|
>
|
||||||
<Text>
|
<Text>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='crypto_donate_panel.intro.message'
|
id='crypto_donate_panel.intro.message'
|
||||||
|
@ -33,18 +45,6 @@ const CryptoDonatePanel: React.FC<ICryptoDonatePanel> = ({ limit = 3 }): JSX.Ele
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<SiteWallet limit={limit} />
|
<SiteWallet limit={limit} />
|
||||||
|
|
||||||
{hasMore && (
|
|
||||||
<Link className='wtf-panel__expand-btn' to='/donate/crypto'>
|
|
||||||
<Text>
|
|
||||||
<FormattedMessage
|
|
||||||
id='crypto_donate_panel.actions.more'
|
|
||||||
defaultMessage='Click to see {count} more {count, plural, one {wallet} other {wallets}}'
|
|
||||||
values={{ count: more }}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -150,6 +150,7 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
display_name: state.display_name,
|
display_name: state.display_name,
|
||||||
website: state.website,
|
website: state.website,
|
||||||
location: state.location,
|
location: state.location,
|
||||||
|
birthday: state.birthday,
|
||||||
note: state.note,
|
note: state.note,
|
||||||
avatar: state.avatar_file,
|
avatar: state.avatar_file,
|
||||||
header: state.header_file,
|
header: state.header_file,
|
||||||
|
@ -263,6 +264,18 @@ class EditProfile extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
{features.birthdays && (
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='edit_profile.fields.birthday_label' defaultMessage='Birthday' />}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name='birthday'
|
||||||
|
value={this.state.birthday}
|
||||||
|
onChange={this.handleTextChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
|
||||||
{features.accountLocation && (
|
{features.accountLocation && (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelText={<FormattedMessage id='edit_profile.fields.location_label' defaultMessage='Location' />}
|
labelText={<FormattedMessage id='edit_profile.fields.location_label' defaultMessage='Location' />}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -6,22 +5,20 @@ import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
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 PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
expandNotifications,
|
expandNotifications,
|
||||||
scrollTopNotifications,
|
scrollTopNotifications,
|
||||||
dequeueNotifications,
|
dequeueNotifications,
|
||||||
} from '../../actions/notifications';
|
} from '../../actions/notifications';
|
||||||
import LoadGap from '../../components/load_gap';
|
|
||||||
import ScrollableList from '../../components/scrollable_list';
|
|
||||||
import TimelineQueueButtonHeader from '../../components/timeline_queue_button_header';
|
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 FilterBarContainer from './containers/filter_bar_container';
|
||||||
import NotificationContainer from './containers/notification_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}}' },
|
queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Footer = ({ context }) => (
|
||||||
|
context.hasMore ? (
|
||||||
|
<PlaceholderNotification />
|
||||||
|
) : null
|
||||||
|
);
|
||||||
|
|
||||||
|
const EmptyPlaceholder = ({ context }) => {
|
||||||
|
if (context.isLoading) {
|
||||||
|
return <PlaceholderNotification />;
|
||||||
|
} else {
|
||||||
|
return <Text>{context.emptyMessage}</Text>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = ({ context, ...rest }) => (
|
||||||
|
<div className='border-solid border-b border-gray-200 dark:border-gray-600' {...rest} />
|
||||||
|
);
|
||||||
|
|
||||||
const getNotifications = createSelector([
|
const getNotifications = createSelector([
|
||||||
state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
|
state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']),
|
||||||
state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']),
|
state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']),
|
||||||
|
@ -48,10 +63,6 @@ const getNotifications = createSelector([
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
const mapStateToProps = state => {
|
||||||
const settings = getSettings(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 {
|
return {
|
||||||
showFilterBar: settings.getIn(['notifications', 'quickFilter', 'show']),
|
showFilterBar: settings.getIn(['notifications', 'quickFilter', 'show']),
|
||||||
|
@ -60,8 +71,6 @@ const mapStateToProps = state => {
|
||||||
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
isUnread: state.getIn(['notifications', 'unread']) > 0,
|
||||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||||
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
totalQueuedNotificationsCount: state.getIn(['notifications', 'totalQueuedNotificationsCount'], 0),
|
||||||
showBirthdayReminders,
|
|
||||||
hasBirthdays: !!birthdays,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,8 +88,6 @@ class Notifications extends React.PureComponent {
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
dequeueNotifications: PropTypes.func,
|
dequeueNotifications: PropTypes.func,
|
||||||
totalQueuedNotificationsCount: PropTypes.number,
|
totalQueuedNotificationsCount: PropTypes.number,
|
||||||
showBirthdayReminders: PropTypes.bool,
|
|
||||||
hasBirthdays: PropTypes.bool,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -117,25 +124,15 @@ class Notifications extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMoveUp = id => {
|
handleMoveUp = id => {
|
||||||
const { hasBirthdays } = this.props;
|
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
||||||
|
|
||||||
let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
|
|
||||||
if (hasBirthdays) elementIndex++;
|
|
||||||
this._selectChild(elementIndex, true);
|
this._selectChild(elementIndex, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMoveDown = id => {
|
handleMoveDown = id => {
|
||||||
const { hasBirthdays } = this.props;
|
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
||||||
|
|
||||||
let elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
|
|
||||||
if (hasBirthdays) elementIndex++;
|
|
||||||
this._selectChild(elementIndex, false);
|
this._selectChild(elementIndex, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMoveBelowBirthdays = () => {
|
|
||||||
this._selectChild(1, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_selectChild(index, align_top) {
|
_selectChild(index, align_top) {
|
||||||
const container = this.column;
|
const container = this.column;
|
||||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||||
|
@ -160,66 +157,42 @@ class Notifications extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount, showBirthdayReminders } = this.props;
|
const { intl, notifications, isLoading, hasMore, showFilterBar, totalQueuedNotificationsCount } = this.props;
|
||||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
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;
|
|
||||||
|
|
||||||
const filterBarContainer = showFilterBar
|
const filterBarContainer = showFilterBar
|
||||||
? (<FilterBarContainer />)
|
? (<FilterBarContainer />)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (isLoading && this.scrollableContent) {
|
const scrollContainer = (
|
||||||
scrollableContent = this.scrollableContent;
|
<PullToRefresh onRefresh={this.handleRefresh}>
|
||||||
} else if (notifications.size > 0 || hasMore) {
|
<Virtuoso
|
||||||
scrollableContent = notifications.map((item, index) => item === null ? (
|
useWindowScroll
|
||||||
<LoadGap
|
data={notifications.toArray()}
|
||||||
key={'gap:' + notifications.getIn([index + 1, 'id'])}
|
startReached={this.handleScrollToTop}
|
||||||
disabled={isLoading}
|
endReached={this.handleLoadOlder}
|
||||||
maxId={index > 0 ? notifications.getIn([index - 1, 'id']) : null}
|
isScrolling={isScrolling => isScrolling && this.handleScroll}
|
||||||
onClick={this.handleLoadGap}
|
itemContent={(_index, notification) => (
|
||||||
|
<NotificationContainer
|
||||||
|
key={notification.id}
|
||||||
|
notification={notification}
|
||||||
|
onMoveUp={this.handleMoveUp}
|
||||||
|
onMoveDown={this.handleMoveDown}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
context={{
|
||||||
|
hasMore,
|
||||||
|
isLoading,
|
||||||
|
emptyMessage,
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
ScrollSeekPlaceholder: PlaceholderNotification,
|
||||||
|
Footer,
|
||||||
|
EmptyPlaceholder,
|
||||||
|
Item,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</PullToRefresh>
|
||||||
<NotificationContainer
|
|
||||||
key={item.get('id')}
|
|
||||||
notification={item}
|
|
||||||
onMoveUp={this.handleMoveUp}
|
|
||||||
onMoveDown={this.handleMoveDown}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
|
|
||||||
if (showBirthdayReminders) scrollableContent = scrollableContent.unshift(
|
|
||||||
<BirthdayReminders
|
|
||||||
key='birthdays'
|
|
||||||
onMoveDown={this.handleMoveBelowBirthdays}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
scrollableContent = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollableContent = scrollableContent;
|
|
||||||
|
|
||||||
const scrollContainer = (
|
|
||||||
<ScrollableList
|
|
||||||
scrollKey='notifications'
|
|
||||||
isLoading={isLoading}
|
|
||||||
showLoading={isLoading && notifications.size === 0}
|
|
||||||
hasMore={hasMore}
|
|
||||||
emptyMessage={emptyMessage}
|
|
||||||
placeholderComponent={PlaceholderNotification}
|
|
||||||
placeholderCount={20}
|
|
||||||
onLoadMore={this.handleLoadOlder}
|
|
||||||
onRefresh={this.handleRefresh}
|
|
||||||
onScrollToTop={this.handleScrollToTop}
|
|
||||||
onScroll={this.handleScroll}
|
|
||||||
className={classNames({
|
|
||||||
'divide-y divide-gray-200 dark:divide-gray-600 divide-solid': notifications.size > 0,
|
|
||||||
'space-y-2': notifications.size === 0,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{scrollableContent}
|
|
||||||
</ScrollableList>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -36,8 +36,16 @@ const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME: This page actually doesn't look good right now
|
||||||
|
// const handleAction = () => {
|
||||||
|
// history.push('/suggestions');
|
||||||
|
// };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Widget title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}>
|
<Widget
|
||||||
|
title={<FormattedMessage id='who_to_follow.title' defaultMessage='People To Follow' />}
|
||||||
|
// onAction={handleAction}
|
||||||
|
>
|
||||||
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
|
{suggestionsToRender.map((suggestion: ImmutableMap<string, any>) => (
|
||||||
<AccountContainer
|
<AccountContainer
|
||||||
key={suggestion.get('account')}
|
key={suggestion.get('account')}
|
||||||
|
|
|
@ -222,6 +222,10 @@ export function BirthdaysModal() {
|
||||||
return import(/* webpackChunkName: "features/ui" */'../components/birthdays_modal');
|
return import(/* webpackChunkName: "features/ui" */'../components/birthdays_modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function BirthdayPanel() {
|
||||||
|
return import(/* webpackChunkName: "features/ui" */'../../../components/birthday-panel');
|
||||||
|
}
|
||||||
|
|
||||||
export function AccountNoteModal() {
|
export function AccountNoteModal() {
|
||||||
return import(/* webpackChunkName: "features/ui" */'../components/account_note_modal');
|
return import(/* webpackChunkName: "features/ui" */'../components/account_note_modal');
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,4 +177,18 @@ describe('normalizeAccount()', () => {
|
||||||
expect(result.staff).toBe(true);
|
expect(result.staff).toBe(true);
|
||||||
expect(result.moderator).toBe(false);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,6 +28,7 @@ export const AccountRecord = ImmutableRecord({
|
||||||
created_at: new Date(),
|
created_at: new Date(),
|
||||||
display_name: '',
|
display_name: '',
|
||||||
emojis: ImmutableList<Emoji>(),
|
emojis: ImmutableList<Emoji>(),
|
||||||
|
favicon: '',
|
||||||
fields: ImmutableList<Field>(),
|
fields: ImmutableList<Field>(),
|
||||||
followers_count: 0,
|
followers_count: 0,
|
||||||
following_count: 0,
|
following_count: 0,
|
||||||
|
@ -52,6 +53,7 @@ export const AccountRecord = ImmutableRecord({
|
||||||
// Internal fields
|
// Internal fields
|
||||||
admin: false,
|
admin: false,
|
||||||
display_name_html: '',
|
display_name_html: '',
|
||||||
|
domain: '',
|
||||||
moderator: false,
|
moderator: false,
|
||||||
note_emojified: '',
|
note_emojified: '',
|
||||||
note_plain: '',
|
note_plain: '',
|
||||||
|
@ -224,6 +226,16 @@ const normalizeFqn = (account: ImmutableMap<string, any>) => {
|
||||||
return account.set('fqn', fqn);
|
return account.set('fqn', fqn);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeFavicon = (account: ImmutableMap<string, any>) => {
|
||||||
|
const favicon = account.getIn(['pleroma', 'favicon']) || '';
|
||||||
|
return account.set('favicon', favicon);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addDomain = (account: ImmutableMap<string, any>) => {
|
||||||
|
const domain = account.get('fqn', '').split('@')[1] || '';
|
||||||
|
return account.set('domain', domain);
|
||||||
|
};
|
||||||
|
|
||||||
const addStaffFields = (account: ImmutableMap<string, any>) => {
|
const addStaffFields = (account: ImmutableMap<string, any>) => {
|
||||||
const admin = account.getIn(['pleroma', 'is_admin']) === true;
|
const admin = account.getIn(['pleroma', 'is_admin']) === true;
|
||||||
const moderator = account.getIn(['pleroma', 'is_moderator']) === true;
|
const moderator = account.getIn(['pleroma', 'is_moderator']) === true;
|
||||||
|
@ -248,6 +260,8 @@ export const normalizeAccount = (account: Record<string, any>) => {
|
||||||
normalizeBirthday(account);
|
normalizeBirthday(account);
|
||||||
normalizeLocation(account);
|
normalizeLocation(account);
|
||||||
normalizeFqn(account);
|
normalizeFqn(account);
|
||||||
|
normalizeFavicon(account);
|
||||||
|
addDomain(account);
|
||||||
addStaffFields(account);
|
addStaffFields(account);
|
||||||
fixUsername(account);
|
fixUsername(account);
|
||||||
fixDisplayName(account);
|
fixDisplayName(account);
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
TrendsPanel,
|
TrendsPanel,
|
||||||
SignUpPanel,
|
SignUpPanel,
|
||||||
CryptoDonatePanel,
|
CryptoDonatePanel,
|
||||||
|
BirthdayPanel,
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
@ -93,6 +94,11 @@ class HomePage extends ImmutablePureComponent {
|
||||||
{Component => <Component limit={cryptoLimit} />}
|
{Component => <Component limit={cryptoLimit} />}
|
||||||
</BundleContainer>
|
</BundleContainer>
|
||||||
)}
|
)}
|
||||||
|
{features.birthdays && (
|
||||||
|
<BundleContainer fetchComponent={BirthdayPanel}>
|
||||||
|
{Component => <Component limit={10} />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
{features.suggestions && (
|
{features.suggestions && (
|
||||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||||
{Component => <Component limit={5} />}
|
{Component => <Component limit={5} />}
|
||||||
|
|
Loading…
Reference in New Issue