kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
commit
4bc6059d2d
|
@ -98,7 +98,7 @@ export function updateNotificationsQueue(notification, intlMessages, intlLocale,
|
|||
|
||||
const isOnNotificationsPage = curPath === '/notifications';
|
||||
|
||||
if (notification.type === 'mention') {
|
||||
if (['mention', 'status'].includes(notification.type)) {
|
||||
const regex = regexFromFilters(filters);
|
||||
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
|
||||
filtered = regex && regex.test(searchIndex);
|
||||
|
@ -170,7 +170,7 @@ export function dequeueNotifications() {
|
|||
const excludeTypesFromSettings = getState => getSettings(getState()).getIn(['notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
|
||||
|
||||
const excludeTypesFromFilter = filter => {
|
||||
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'poll', 'move', 'pleroma:emoji_reaction']);
|
||||
const allTypes = ImmutableList(['follow', 'follow_request', 'favourite', 'reblog', 'mention', 'status', 'poll', 'move', 'pleroma:emoji_reaction']);
|
||||
return allTypes.filterNot(item => item === filter).toJS();
|
||||
};
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ interface IAccount {
|
|||
showProfileHoverCard?: boolean,
|
||||
timestamp?: string | Date,
|
||||
timestampUrl?: string,
|
||||
withDate?: boolean,
|
||||
withRelationship?: boolean,
|
||||
}
|
||||
|
||||
|
@ -51,6 +52,7 @@ const Account = ({
|
|||
showProfileHoverCard = true,
|
||||
timestamp,
|
||||
timestampUrl,
|
||||
withDate = false,
|
||||
withRelationship = true,
|
||||
}: IAccount) => {
|
||||
const overflowRef = React.useRef<HTMLDivElement>(null);
|
||||
|
@ -122,6 +124,8 @@ const Account = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (withDate) timestamp = account.created_at;
|
||||
|
||||
const LinkEl: any = showProfileHoverCard ? Link : 'div';
|
||||
|
||||
return (
|
||||
|
|
|
@ -206,8 +206,8 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
key={key}
|
||||
data-index={i}
|
||||
className={classNames({
|
||||
'px-4 py-2.5 text-sm text-gray-700 cursor-pointer hover:bg-gray-100 group': true,
|
||||
'bg-gray-100 hover:bg-gray-100': i === selectedSuggestion,
|
||||
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 group': true,
|
||||
'bg-gray-100 dark:bg-slate-700 hover:bg-gray-100 dark:hover:bg-gray-700': i === selectedSuggestion,
|
||||
})}
|
||||
onMouseDown={this.onSuggestionClick}
|
||||
>
|
||||
|
@ -238,7 +238,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
|
||||
return menu.map((item, i) => (
|
||||
<a
|
||||
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 cursor-pointer hover:bg-gray-100', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 dark:text-gray-400 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
href='#'
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
|
@ -293,7 +293,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
/>
|
||||
|
||||
<div className={classNames({
|
||||
'absolute top-full w-full z-50 shadow bg-white rounded-lg py-1': true,
|
||||
'absolute top-full w-full z-50 shadow bg-white dark:bg-slate-800 rounded-lg py-1': true,
|
||||
hidden: !visible,
|
||||
block: visible,
|
||||
'autosuggest-textarea__suggestions--visible': visible,
|
||||
|
|
|
@ -19,7 +19,7 @@ interface IInput extends Pick<React.InputHTMLAttributes<HTMLInputElement>, 'onCh
|
|||
name?: string,
|
||||
placeholder?: string,
|
||||
value?: string,
|
||||
onChange?: () => void,
|
||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void,
|
||||
type: 'text' | 'email' | 'tel' | 'password'
|
||||
}
|
||||
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
import { is } from 'immutable';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import compareId from 'soapbox/compare_id';
|
||||
import AccountListPanel from 'soapbox/features/ui/components/account_list_panel';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||
expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const accountIds = state.getIn(['admin', 'latestUsers']);
|
||||
|
||||
// HACK: AdminAPI only recently started sorting new users at the top.
|
||||
// Try a dirty check to see if the users are sorted properly, or don't show the panel.
|
||||
// Probably works most of the time.
|
||||
const sortedIds = accountIds.sort(compareId).reverse();
|
||||
const hasDates = accountIds.every(id => state.getIn(['accounts', id, 'created_at']));
|
||||
const isSorted = hasDates && is(accountIds, sortedIds);
|
||||
|
||||
return {
|
||||
isSorted,
|
||||
accountIds,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class LatestAccountsPanel extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountIds: ImmutablePropTypes.orderedSet.isRequired,
|
||||
limit: PropTypes.number,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
limit: 5,
|
||||
}
|
||||
|
||||
state = {
|
||||
total: 0,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, limit } = this.props;
|
||||
|
||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
||||
.then(({ count }) => {
|
||||
this.setState({ total: count });
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, accountIds, limit, isSorted, ...props } = this.props;
|
||||
const { total } = this.state;
|
||||
|
||||
if (!isSorted || !accountIds || accountIds.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expandCount = total - accountIds.size;
|
||||
|
||||
return (
|
||||
<AccountListPanel
|
||||
icon={require('@tabler/icons/icons/users.svg')}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
accountIds={accountIds}
|
||||
limit={limit}
|
||||
total={total}
|
||||
expandMessage={intl.formatMessage(messages.expand, { count: expandCount })}
|
||||
expandRoute='/admin/users'
|
||||
withDate
|
||||
withRelationship={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
import { OrderedSet as ImmutableOrderedSet, is } from 'immutable';
|
||||
import React, { useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import compareId from 'soapbox/compare_id';
|
||||
import { Text, Widget } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'admin.latest_accounts_panel.title', defaultMessage: 'Latest Accounts' },
|
||||
expand: { id: 'admin.latest_accounts_panel.expand_message', defaultMessage: 'Click to see {count} more {count, plural, one {account} other {accounts}}' },
|
||||
});
|
||||
|
||||
interface ILatestAccountsPanel {
|
||||
limit?: number,
|
||||
}
|
||||
|
||||
const LatestAccountsPanel: React.FC<ILatestAccountsPanel> = ({ limit = 5 }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountIds = useAppSelector<ImmutableOrderedSet<string>>((state) => state.admin.get('latestUsers').take(limit));
|
||||
const hasDates = useAppSelector((state) => accountIds.every(id => !!state.accounts.getIn([id, 'created_at'])));
|
||||
|
||||
const [total, setTotal] = useState(accountIds.size);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchUsers(['local', 'active'], 1, null, limit))
|
||||
.then((value) => {
|
||||
setTotal((value as { count: number }).count);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const sortedIds = accountIds.sort(compareId).reverse();
|
||||
const isSorted = hasDates && is(accountIds, sortedIds);
|
||||
|
||||
if (!isSorted || !accountIds || accountIds.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const expandCount = total - accountIds.size;
|
||||
|
||||
return (
|
||||
<Widget title={intl.formatMessage(messages.title)}>
|
||||
{accountIds.take(limit).map((account) => (
|
||||
<AccountContainer key={account} id={account} withRelationship={false} withDate />
|
||||
))}
|
||||
{!!expandCount && (
|
||||
<Link className='wtf-panel__expand-btn' to='/admin/users'>
|
||||
<Text>{intl.formatMessage(messages.expand, { count: expandCount })}</Text>
|
||||
</Link>
|
||||
)}
|
||||
</Widget>
|
||||
);
|
||||
};
|
||||
|
||||
export default LatestAccountsPanel;
|
|
@ -7,6 +7,7 @@ import { connect } from 'react-redux';
|
|||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email_list';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { parseVersion } from 'soapbox/utils/features';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
@ -86,56 +87,46 @@ class Dashboard extends ImmutablePureComponent {
|
|||
<Column icon='tachometer-alt' label={intl.formatMessage(messages.heading)}>
|
||||
<div className='dashcounters'>
|
||||
{mau && <div className='dashcounter'>
|
||||
<div>
|
||||
<div className='dashcounter__num'>
|
||||
<FormattedNumber value={mau} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' />
|
||||
</div>
|
||||
</div>
|
||||
<Text align='center' size='2xl' weight='medium'>
|
||||
<FormattedNumber value={mau} />
|
||||
</Text>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='admin.dashcounters.mau_label' defaultMessage='monthly active users' />
|
||||
</Text>
|
||||
</div>}
|
||||
<div className='dashcounter'>
|
||||
<Link to='/admin/users'>
|
||||
<div className='dashcounter__num'>
|
||||
<FormattedNumber value={userCount} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<Link className='dashcounter' to='/admin/users'>
|
||||
<Text align='center' size='2xl' weight='medium'>
|
||||
<FormattedNumber value={userCount} />
|
||||
</Text>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='admin.dashcounters.user_count_label' defaultMessage='total users' />
|
||||
</Text>
|
||||
</Link>
|
||||
{isNumber(retention) && (
|
||||
<div className='dashcounter'>
|
||||
<div>
|
||||
<div className='dashcounter__num'>
|
||||
{retention}%
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
||||
</div>
|
||||
</div>
|
||||
<Text align='center' size='2xl' weight='medium'>
|
||||
{retention}%
|
||||
</Text>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='admin.dashcounters.retention_label' defaultMessage='user retention' />
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
<Link className='dashcounter' to='/timeline/local'>
|
||||
<Text align='center' size='2xl' weight='medium'>
|
||||
<FormattedNumber value={instance.getIn(['stats', 'status_count'])} />
|
||||
</Text>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />
|
||||
</Text>
|
||||
</Link>
|
||||
<div className='dashcounter'>
|
||||
<Link to='/timeline/local'>
|
||||
<div className='dashcounter__num'>
|
||||
<FormattedNumber value={instance.getIn(['stats', 'status_count'])} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.status_count_label' defaultMessage='posts' />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='dashcounter'>
|
||||
<div>
|
||||
<div className='dashcounter__num'>
|
||||
<FormattedNumber value={instance.getIn(['stats', 'domain_count'])} />
|
||||
</div>
|
||||
<div className='dashcounter__label'>
|
||||
<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />
|
||||
</div>
|
||||
</div>
|
||||
<Text align='center' size='2xl' weight='medium'>
|
||||
<FormattedNumber value={instance.getIn(['stats', 'domain_count'])} />
|
||||
</Text>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='admin.dashcounters.domain_count_label' defaultMessage='peers' />
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{account.admin && <RegistrationModePicker />}
|
||||
|
|
|
@ -116,6 +116,7 @@ class UserIndex extends ImmutablePureComponent {
|
|||
showLoading={showLoading}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
className='mt-4 space-y-4'
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<AccountContainer key={id} id={id} withDate />,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { deleteAccount } from 'soapbox/actions/security';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { Button, Card, CardBody, CardHeader, CardTitle, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
passwordFieldLabel: { id: 'security.fields.password.label', defaultMessage: 'Password' },
|
||||
|
@ -18,12 +18,12 @@ const messages = defineMessages({
|
|||
|
||||
const DeleteAccount = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [password, setPassword] = React.useState('');
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
|
||||
const handleInputChange = React.useCallback((event) => {
|
||||
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
event.persist();
|
||||
|
||||
setPassword(event.target.value);
|
|
@ -1,92 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeSettingImmediate } from 'soapbox/actions/settings';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { Button, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers', defaultMessage: 'Developers' },
|
||||
answerLabel: { id: 'developers.challenge.answer_label', defaultMessage: 'Answer' },
|
||||
answerPlaceholder: { id: 'developers.challenge.answer_placeholder', defaultMessage: 'Your answer' },
|
||||
success: { id: 'developers.challenge.success', defaultMessage: 'You are now a developer' },
|
||||
fail: { id: 'developers.challenge.fail', defaultMessage: 'Wrong answer' },
|
||||
});
|
||||
|
||||
export default @connect()
|
||||
@injectIntl
|
||||
class DevelopersChallenge extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
answer: '',
|
||||
}
|
||||
|
||||
handleChangeAnswer = e => {
|
||||
this.setState({ answer: e.target.value });
|
||||
}
|
||||
|
||||
handleSubmit = e => {
|
||||
const { intl, dispatch } = this.props;
|
||||
const { answer } = this.state;
|
||||
|
||||
if (answer === 'boxsoap') {
|
||||
dispatch(changeSettingImmediate(['isDeveloper'], true));
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.success)));
|
||||
} else {
|
||||
dispatch(snackbar.error(intl.formatMessage(messages.fail)));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props;
|
||||
|
||||
const challenge = `function soapbox() {
|
||||
return 'soap|box'.split('|').reverse().join('');
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='developers.challenge.message'
|
||||
defaultMessage='What is the result of calling {function}?'
|
||||
values={{ function: <span className='font-mono'>soapbox()</span> }}
|
||||
/>
|
||||
</Text>
|
||||
<Text tag='pre' family='mono'>
|
||||
{challenge}
|
||||
</Text>
|
||||
|
||||
<FormGroup
|
||||
labelText={intl.formatMessage(messages.answerLabel)}
|
||||
>
|
||||
<Input
|
||||
name='answer'
|
||||
placeholder={intl.formatMessage(messages.answerPlaceholder)}
|
||||
onChange={this.handleChangeAnswer}
|
||||
value={this.state.answer}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='developers.challenge.submit' defaultMessage='Become a developer' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { changeSettingImmediate } from 'soapbox/actions/settings';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { Button, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers', defaultMessage: 'Developers' },
|
||||
answerLabel: { id: 'developers.challenge.answer_label', defaultMessage: 'Answer' },
|
||||
answerPlaceholder: { id: 'developers.challenge.answer_placeholder', defaultMessage: 'Your answer' },
|
||||
success: { id: 'developers.challenge.success', defaultMessage: 'You are now a developer' },
|
||||
fail: { id: 'developers.challenge.fail', defaultMessage: 'Wrong answer' },
|
||||
});
|
||||
|
||||
const DevelopersChallenge = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const [answer, setAnswer] = useState('');
|
||||
|
||||
const handleChangeAnswer = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setAnswer(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (answer === 'boxsoap') {
|
||||
dispatch(changeSettingImmediate(['isDeveloper'], true));
|
||||
dispatch(snackbar.success(intl.formatMessage(messages.success)));
|
||||
} else {
|
||||
dispatch(snackbar.error(intl.formatMessage(messages.fail)));
|
||||
}
|
||||
};
|
||||
|
||||
const challenge = `function soapbox() {
|
||||
return 'soap|box'.split('|').reverse().join('');
|
||||
}`;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='developers.challenge.message'
|
||||
defaultMessage='What is the result of calling {function}?'
|
||||
values={{ function: <span className='font-mono'>soapbox()</span> }}
|
||||
/>
|
||||
</Text>
|
||||
<Text tag='pre' family='mono'>
|
||||
{challenge}
|
||||
</Text>
|
||||
|
||||
<FormGroup
|
||||
labelText={intl.formatMessage(messages.answerLabel)}
|
||||
>
|
||||
<Input
|
||||
name='answer'
|
||||
placeholder={intl.formatMessage(messages.answerPlaceholder)}
|
||||
onChange={handleChangeAnswer}
|
||||
value={answer}
|
||||
type='text'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='primary' type='submit'>
|
||||
<FormattedMessage id='developers.challenge.submit' defaultMessage='Become a developer' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Form>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default DevelopersChallenge;
|
|
@ -20,7 +20,7 @@ const Developers = () => {
|
|||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const leaveDevelopers = (e) => {
|
||||
const leaveDevelopers = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(changeSettingImmediate(['isDeveloper'], false));
|
|
@ -1,30 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
|
||||
import DevelopersChallenge from './developers_challenge';
|
||||
import DevelopersMenu from './developers_menu';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const settings = getSettings(state);
|
||||
|
||||
return {
|
||||
isDeveloper: settings.get('isDeveloper'),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Developers extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
isDeveloper: PropTypes.bool.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isDeveloper } = this.props;
|
||||
return isDeveloper ? <DevelopersMenu /> : <DevelopersChallenge />;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import DevelopersChallenge from './developers_challenge';
|
||||
import DevelopersMenu from './developers_menu';
|
||||
|
||||
const Developers: React.FC = () => {
|
||||
const isDeveloper = useAppSelector((state) => getSettings(state).get('isDeveloper'));
|
||||
|
||||
return isDeveloper ? <DevelopersMenu /> : <DevelopersChallenge />;
|
||||
};
|
||||
|
||||
export default Developers;
|
|
@ -15,6 +15,7 @@ const messages = defineMessages({
|
|||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
moves: { id: 'notifications.filter.moves', defaultMessage: 'Moves' },
|
||||
emoji_reacts: { id: 'notifications.filter.emoji_reacts', defaultMessage: 'Emoji reacts' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
|
@ -80,6 +81,12 @@ class NotificationFilterBar extends React.PureComponent {
|
|||
action: this.onClick('poll'),
|
||||
name: 'poll',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon src={require('@tabler/icons/icons/home.svg')} />,
|
||||
title: intl.formatMessage(messages.statuses),
|
||||
action: this.onClick('status'),
|
||||
name: 'status',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon src={require('@tabler/icons/icons/user-plus.svg')} />,
|
||||
title: intl.formatMessage(messages.follows),
|
||||
|
|
|
@ -36,13 +36,14 @@ const buildLink = (account) => (
|
|||
</bdi>
|
||||
);
|
||||
|
||||
export const NOTIFICATION_TYPES = ['follow', 'mention', 'favourite', 'reblog'];
|
||||
export const NOTIFICATION_TYPES = ['follow', 'mention', 'favourite', 'reblog', 'status'];
|
||||
|
||||
const icons = {
|
||||
follow: require('@tabler/icons/icons/user-plus.svg'),
|
||||
mention: require('@tabler/icons/icons/at.svg'),
|
||||
favourite: require('@tabler/icons/icons/heart.svg'),
|
||||
reblog: require('@tabler/icons/icons/repeat.svg'),
|
||||
status: require('@tabler/icons/icons/home.svg'),
|
||||
};
|
||||
|
||||
const messages = {
|
||||
|
@ -62,6 +63,10 @@ const messages = {
|
|||
id: 'notification.reblog',
|
||||
defaultMessage: '{name} re-TRUTH your TRUTH',
|
||||
},
|
||||
status: {
|
||||
id: 'notification.status',
|
||||
defaultMessage: '{name} just posted',
|
||||
},
|
||||
};
|
||||
|
||||
const buildMessage = (type, account) => {
|
||||
|
@ -153,6 +158,7 @@ const Notification = (props) => {
|
|||
case 'favourite':
|
||||
case 'mention':
|
||||
case 'reblog':
|
||||
case 'status':
|
||||
return (
|
||||
<StatusContainer
|
||||
id={notification.getIn(['status', 'id'])}
|
||||
|
|
|
@ -46,7 +46,7 @@ const ActionsModal = ({ status, actions, onClick, onClose }) => {
|
|||
{({ top }) => (
|
||||
<div className='modal-root__modal actions-modal' style={{ top: `${top}%` }}>
|
||||
{status && (
|
||||
<Stack space={2} className='p-4 bg-gray-50 border-b border-solid border-gray-200'>
|
||||
<Stack space={2} className='p-4 bg-gray-50 dark:bg-slate-800 border-b border-solid border-gray-200 dark:border-gray-700'>
|
||||
<AccountContainer
|
||||
account={status.get('account')}
|
||||
showProfileHoverCard={false}
|
||||
|
|
|
@ -3,16 +3,14 @@ import React from 'react';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { remoteInteraction } from 'soapbox/actions/interactions';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
import { Button, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import { Modal, Stack, Text } from '../../../components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
accountPlaceholder: { id: 'remote_interaction.account_placeholder', defaultMessage: 'Enter your username@domain you want to act from' },
|
||||
|
@ -133,11 +131,14 @@ class UnauthorizedModal extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal compose-modal unauthorized-modal remote-interaction-modal'>
|
||||
<div className='compose-modal__header'>
|
||||
<h3 className='compose-modal__header__title'>{header}</h3>
|
||||
<IconButton className='compose-modal__close' title={intl.formatMessage(messages.close)} src={require('@tabler/icons/icons/x.svg')} onClick={this.onClickClose} />
|
||||
</div>
|
||||
<Modal
|
||||
title={header}
|
||||
onClose={this.onClickClose}
|
||||
confirmationAction={!singleUserMode && this.onLogin}
|
||||
confirmationText={<FormattedMessage id='account.login' defaultMessage='Log in' />}
|
||||
secondaryAction={this.onRegister}
|
||||
secondaryText={<FormattedMessage id='account.register' defaultMessage='Sign up' />}
|
||||
>
|
||||
<div className='remote-interaction-modal__content'>
|
||||
<form className='simple_form remote-interaction-modal__fields'>
|
||||
<input
|
||||
|
@ -150,26 +151,20 @@ class UnauthorizedModal extends ImmutablePureComponent {
|
|||
onChange={this.onAccountChange}
|
||||
required
|
||||
/>
|
||||
<button className='button' onClick={this.onClickProceed}>{button}</button>
|
||||
<Button theme='primary' onClick={this.onClickProceed}>{button}</Button>
|
||||
</form>
|
||||
<div className='remote-interaction-modal__divider'>
|
||||
<span>
|
||||
<Text align='center'>
|
||||
<FormattedMessage id='remote_interaction.divider' defaultMessage='or' />
|
||||
</span>
|
||||
</Text>
|
||||
</div>
|
||||
{!singleUserMode && (
|
||||
<>
|
||||
<h3 className='compose-modal__header__title'><FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} /></h3>
|
||||
<Link to='/' className='unauthorized-modal-content__button button' onClick={this.onClickClose}>
|
||||
<FormattedMessage id='account.register' defaultMessage='Sign up' />
|
||||
</Link>
|
||||
</>
|
||||
<Text size='lg' weight='medium'>
|
||||
<FormattedMessage id='unauthorized_modal.title' defaultMessage='Sign up for {site_title}' values={{ site_title: siteTitle }} />
|
||||
</Text>
|
||||
)}
|
||||
<Link to='/login' className='unauthorized-modal-content__button button button-secondary' onClick={this.onClickClose}>
|
||||
<FormattedMessage id='account.login' defaultMessage='Log in' />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
export { useAppDispatch } from './useAppDispatch';
|
||||
export { useAppSelector } from './useAppSelector';
|
||||
export { useFeatures } from './useFeatures';
|
||||
export { useOnScreen } from './useOnScreen';
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { AppDispatch } from 'soapbox/store';
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
|
@ -694,6 +694,7 @@
|
|||
"notification.pleroma:emoji_reaction": "{name} zareagował(a) na Twój wpis",
|
||||
"notification.poll": "Głosowanie w którym brałeś(-aś) udział zakończyła się",
|
||||
"notification.reblog": "{name} podbił(a) Twój wpis",
|
||||
"notification.status": "{name} właśnie opublikował(a) wpis",
|
||||
"notifications.clear": "Wyczyść powiadomienia",
|
||||
"notifications.clear_confirmation": "Czy na pewno chcesz bezpowrotnie usunąć wszystkie powiadomienia?",
|
||||
"notifications.clear_heading": "Wyczyść powiadomienia",
|
||||
|
@ -725,6 +726,7 @@
|
|||
"notifications.filter.mentions": "Wspomienia",
|
||||
"notifications.filter.moves": "Przenoszone konta",
|
||||
"notifications.filter.polls": "Wyniki głosowania",
|
||||
"notifications.filter.statuses": "Nowe wpisy osób, które subskrybujesz",
|
||||
"notifications.group": "{count, number} {count, plural, one {powiadomienie} few {powiadomienia} many {powiadomień} more {powiadomień}}",
|
||||
"notifications.queue_label": "Naciśnij aby zobaczyć {count} {count, plural, one {nowe powiadomienie} few {nowe powiadomienia} many {nowych powiadomień} other {nowe powiadomienia}}",
|
||||
"password_reset.confirmation": "Sprawdź swoją pocztę e-mail, aby potwierdzić.",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { Record as ImmutableRecord } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
ACCOUNT_NOTE_INIT_MODAL,
|
||||
|
@ -8,15 +9,19 @@ import {
|
|||
ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
} from '../actions/account_notes';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
edit: ImmutableMap({
|
||||
isSubmitting: false,
|
||||
account: null,
|
||||
comment: null,
|
||||
}),
|
||||
const EditRecord = ImmutableRecord({
|
||||
isSubmitting: false,
|
||||
account: null,
|
||||
comment: null,
|
||||
});
|
||||
|
||||
export default function account_notes(state = initialState, action) {
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
edit: EditRecord(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
export default function account_notes(state: State = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case ACCOUNT_NOTE_INIT_MODAL:
|
||||
return state.withMutations((state) => {
|
|
@ -73,7 +73,7 @@ const isValid = notification => {
|
|||
}
|
||||
|
||||
// Mastodon can return status notifications with a null status
|
||||
if (['mention', 'reblog', 'favourite', 'poll'].includes(notification.type) && !notification.status.id) {
|
||||
if (['mention', 'reblog', 'favourite', 'poll', 'status'].includes(notification.type) && !notification.status.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,65 +1,9 @@
|
|||
.dashcounters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
||||
margin: 0 -5px 0;
|
||||
padding: 20px;
|
||||
@apply grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 mb-4;
|
||||
}
|
||||
|
||||
.dashcounter {
|
||||
box-sizing: border-box;
|
||||
flex: 0 0 33.333%;
|
||||
padding: 0 5px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
> a,
|
||||
> div {
|
||||
box-sizing: border-box;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
background: var(--accent-color--faint);
|
||||
border-radius: 4px;
|
||||
transition: 0.2s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
> a:hover {
|
||||
background: var(--accent-color--med);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&__num,
|
||||
&__icon,
|
||||
&__text {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
color: var(--primary-text-color);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.svg-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
||||
svg {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 14px;
|
||||
color: hsla(var(--primary-text-color_hsl), 0.6);
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
@apply bg-gray-200 dark:bg-gray-600 p-4 rounded flex flex-col items-center space-y-2 hover:-translate-y-1 transition-transform cursor-pointer;
|
||||
}
|
||||
|
||||
.dashwidgets {
|
||||
|
|
|
@ -341,7 +341,7 @@
|
|||
|
||||
.actions-modal {
|
||||
.dropdown-menu__separator {
|
||||
@apply block m-2 h-[1px] bg-gray-200;
|
||||
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -521,7 +521,7 @@
|
|||
}
|
||||
|
||||
.actions-modal {
|
||||
@apply w-full max-h-full max-w-lg mt-auto mb-2 bg-white;
|
||||
@apply w-full max-h-full max-w-lg mt-auto mb-2 bg-white dark:bg-slate-800;
|
||||
|
||||
.status {
|
||||
overflow-y: auto;
|
||||
|
@ -540,7 +540,7 @@
|
|||
li:not(:empty) {
|
||||
a,
|
||||
button {
|
||||
@apply flex items-center px-4 py-3 text-gray-600 no-underline hover:bg-gray-100 hover:text-gray-800;
|
||||
@apply flex items-center px-4 py-3 text-gray-600 dark:text-gray-300 no-underline hover:bg-gray-100 dark:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-200;
|
||||
|
||||
&.destructive {
|
||||
@apply text-danger-600;
|
||||
|
@ -817,9 +817,7 @@
|
|||
&__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// align-items: center;
|
||||
row-gap: 10px;
|
||||
padding: 10px;
|
||||
|
||||
.unauthorized-modal-content__button {
|
||||
margin: 0 auto;
|
||||
|
@ -832,11 +830,8 @@
|
|||
gap: 10px;
|
||||
width: 100%;
|
||||
|
||||
.button {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
text-transform: none;
|
||||
overflow: unset;
|
||||
button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -848,9 +843,9 @@
|
|||
|
||||
&::before,
|
||||
&::after {
|
||||
@apply border-b border-gray-300 dark:border-gray-600;
|
||||
content: "";
|
||||
flex: 1;
|
||||
border-bottom: 1px solid hsla(var(--primary-text-color_hsl), 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.reply-mentions {
|
||||
@apply text-gray-500 mb-1 text-sm;
|
||||
@apply text-gray-500 dark:text-gray-400 mb-1 text-sm;
|
||||
|
||||
&__account {
|
||||
@apply text-primary-600 no-underline;
|
||||
|
|
|
@ -139,13 +139,13 @@
|
|||
}
|
||||
|
||||
&__expand-btn {
|
||||
@apply border-gray-300 dark:border-gray-600;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 46px;
|
||||
position: relative;
|
||||
border-top: 1px solid;
|
||||
border-color: var(--brand-color--faint);
|
||||
transition: max-height 150ms ease;
|
||||
overflow: hidden;
|
||||
opacity: 1;
|
||||
|
|
Ładowanie…
Reference in New Issue