Merge remote-tracking branch 'soapbox/develop' into events
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
commit
ea4f7a7332
44 changed files with 471 additions and 490 deletions
15
app/soapbox/__fixtures__/status-quotes.json
Normal file
15
app/soapbox/__fixtures__/status-quotes.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
[
|
||||
{
|
||||
"account": {
|
||||
"id": "ABDSjI3Q0R8aDaz1U0"
|
||||
},
|
||||
"content": "quoast",
|
||||
"id": "AJsajx9hY4Q7IKQXEe",
|
||||
"pleroma": {
|
||||
"quote": {
|
||||
"content": "<p>10</p>",
|
||||
"id": "AJmoVikzI3SkyITyim"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
150
app/soapbox/actions/__tests__/status-quotes.test.ts
Normal file
150
app/soapbox/actions/__tests__/status-quotes.test.ts
Normal file
|
@ -0,0 +1,150 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { mockStore, rootState } from 'soapbox/jest/test-helpers';
|
||||
import { StatusListRecord } from 'soapbox/reducers/status-lists';
|
||||
|
||||
import { fetchStatusQuotes, expandStatusQuotes } from '../status-quotes';
|
||||
|
||||
const status = {
|
||||
account: {
|
||||
id: 'ABDSjI3Q0R8aDaz1U0',
|
||||
},
|
||||
content: 'quoast',
|
||||
id: 'AJsajx9hY4Q7IKQXEe',
|
||||
pleroma: {
|
||||
quote: {
|
||||
content: '<p>10</p>',
|
||||
id: 'AJmoVikzI3SkyITyim',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const statusId = 'AJmoVikzI3SkyITyim';
|
||||
|
||||
describe('fetchStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '1234');
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_FETCH_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet(`/api/v1/pleroma/statuses/${statusId}/quotes`).networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_FETCH_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_FETCH_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(fetchStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandStatusQuotes()', () => {
|
||||
let store: ReturnType<typeof mockStore>;
|
||||
|
||||
describe('without a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState
|
||||
.set('me', '1234')
|
||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: null }) }));
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
it('should do nothing', async() => {
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a url', () => {
|
||||
beforeEach(() => {
|
||||
const state = rootState.set('me', '1234')
|
||||
.set('status_lists', ImmutableMap({ [`quotes:${statusId}`]: StatusListRecord({ next: 'example' }) }));
|
||||
store = mockStore(state);
|
||||
});
|
||||
|
||||
describe('with a successful API request', () => {
|
||||
beforeEach(() => {
|
||||
const quotes = require('soapbox/__fixtures__/status-quotes.json');
|
||||
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').reply(200, quotes, {
|
||||
link: `<https://example.com/api/v1/pleroma/statuses/${statusId}/quotes?since_id=1>; rel='prev'`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch quotes from the API', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'POLLS_IMPORT', polls: [] },
|
||||
{ type: 'ACCOUNTS_IMPORT', accounts: [status.account] },
|
||||
{ type: 'STATUSES_IMPORT', statuses: [status], expandSpoilers: false },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_SUCCESS', statusId, statuses: [status], next: null },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with an unsuccessful API request', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('example').networkError();
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch failed action', async() => {
|
||||
const expectedActions = [
|
||||
{ type: 'STATUS_QUOTES_EXPAND_REQUEST', statusId },
|
||||
{ type: 'STATUS_QUOTES_EXPAND_FAIL', statusId, error: new Error('Network Error') },
|
||||
];
|
||||
await store.dispatch(expandStatusQuotes(statusId));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
75
app/soapbox/actions/status-quotes.ts
Normal file
75
app/soapbox/actions/status-quotes.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import api, { getLinks } from '../api';
|
||||
|
||||
import { importFetchedStatuses } from './importer';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||
|
||||
export const STATUS_QUOTES_FETCH_REQUEST = 'STATUS_QUOTES_FETCH_REQUEST';
|
||||
export const STATUS_QUOTES_FETCH_SUCCESS = 'STATUS_QUOTES_FETCH_SUCCESS';
|
||||
export const STATUS_QUOTES_FETCH_FAIL = 'STATUS_QUOTES_FETCH_FAIL';
|
||||
|
||||
export const STATUS_QUOTES_EXPAND_REQUEST = 'STATUS_QUOTES_EXPAND_REQUEST';
|
||||
export const STATUS_QUOTES_EXPAND_SUCCESS = 'STATUS_QUOTES_EXPAND_SUCCESS';
|
||||
export const STATUS_QUOTES_EXPAND_FAIL = 'STATUS_QUOTES_EXPAND_FAIL';
|
||||
|
||||
const noOp = () => new Promise(f => f(null));
|
||||
|
||||
export const fetchStatusQuotes = (statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
statusId,
|
||||
type: STATUS_QUOTES_FETCH_REQUEST,
|
||||
});
|
||||
|
||||
return api(getState).get(`/api/v1/pleroma/statuses/${statusId}/quotes`).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return dispatch({
|
||||
type: STATUS_QUOTES_FETCH_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
type: STATUS_QUOTES_FETCH_FAIL,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const expandStatusQuotes = (statusId: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const url = getState().status_lists.getIn([`quotes:${statusId}`, 'next'], null) as string | null;
|
||||
|
||||
if (url === null || getState().status_lists.getIn([`quotes:${statusId}`, 'isLoading'])) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: STATUS_QUOTES_EXPAND_REQUEST,
|
||||
statusId,
|
||||
});
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
dispatch({
|
||||
type: STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
statusId,
|
||||
statuses: response.data,
|
||||
next: next ? next.uri : null,
|
||||
});
|
||||
}).catch(error => {
|
||||
dispatch({
|
||||
type: STATUS_QUOTES_EXPAND_FAIL,
|
||||
statusId,
|
||||
error,
|
||||
});
|
||||
});
|
||||
};
|
|
@ -1,129 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
// import classNames from 'clsx';
|
||||
// import { injectIntl, defineMessages } from 'react-intl';
|
||||
// import Icon from 'soapbox/components/icon';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
|
||||
// const messages = defineMessages({
|
||||
// show: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
// hide: { id: 'column_header.hide_settings', defaultMessage: 'Hide settings' },
|
||||
// });
|
||||
|
||||
class ColumnHeader extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
// intl: PropTypes.object.isRequired,
|
||||
title: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
extraButton: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
state = {
|
||||
collapsed: true,
|
||||
animating: false,
|
||||
};
|
||||
|
||||
historyBack = () => {
|
||||
if (window.history?.length === 1) {
|
||||
this.props.history.push('/');
|
||||
} else {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
handleToggleClick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.setState({ collapsed: !this.state.collapsed, animating: true });
|
||||
}
|
||||
|
||||
handleBackClick = () => {
|
||||
this.historyBack();
|
||||
}
|
||||
|
||||
handleTransitionEnd = () => {
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title } = this.props;
|
||||
|
||||
return <SubNavigation message={title} />;
|
||||
}
|
||||
|
||||
// render() {
|
||||
// const { title, icon, active, children, extraButton, intl: { formatMessage } } = this.props;
|
||||
// const { collapsed, animating } = this.state;
|
||||
//
|
||||
// const wrapperClassName = classNames('column-header__wrapper', {
|
||||
// 'active': active,
|
||||
// });
|
||||
//
|
||||
// const buttonClassName = classNames('column-header', {
|
||||
// 'active': active,
|
||||
// });
|
||||
//
|
||||
// const collapsibleClassName = classNames('column-header__collapsible', {
|
||||
// 'collapsed': collapsed,
|
||||
// 'animating': animating,
|
||||
// });
|
||||
//
|
||||
// const collapsibleButtonClassName = classNames('column-header__button', {
|
||||
// 'active': !collapsed,
|
||||
// });
|
||||
//
|
||||
// let extraContent, collapseButton;
|
||||
//
|
||||
// if (children) {
|
||||
// extraContent = (
|
||||
// <div key='extra-content' className='column-header__collapsible__extra'>
|
||||
// {children}
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// const collapsedContent = [
|
||||
// extraContent,
|
||||
// ];
|
||||
//
|
||||
// if (children) {
|
||||
// collapseButton = <button className={collapsibleButtonClassName} title={formatMessage(collapsed ? messages.show : messages.hide)} aria-label={formatMessage(collapsed ? messages.show : messages.hide)} aria-pressed={collapsed ? 'false' : 'true'} onClick={this.handleToggleClick}><Icon id='cog' /></button>;
|
||||
// }
|
||||
//
|
||||
// const hasTitle = icon && title;
|
||||
//
|
||||
// return (
|
||||
// <div className={wrapperClassName}>
|
||||
// <h1 className={buttonClassName}>
|
||||
// {hasTitle && (
|
||||
// <button>
|
||||
// <Icon id={icon} fixedWidth className='column-header__icon' />
|
||||
// {title}
|
||||
// </button>
|
||||
// )}
|
||||
//
|
||||
// <div className='column-header__buttons'>
|
||||
// {extraButton}
|
||||
// {collapseButton}
|
||||
// </div>
|
||||
// </h1>
|
||||
//
|
||||
// <div className={collapsibleClassName} tabIndex={collapsed ? -1 : null} onTransitionEnd={this.handleTransitionEnd}>
|
||||
// <div className='column-header__collapsible-inner'>
|
||||
// {(!collapsed || animating) && collapsedContent}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(ColumnHeader);
|
|
@ -1,41 +0,0 @@
|
|||
// import throttle from 'lodash/throttle';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
// import { connect } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { CardHeader, CardTitle } from './ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
});
|
||||
|
||||
interface ISubNavigation {
|
||||
message: React.ReactNode,
|
||||
/** @deprecated Unused. */
|
||||
settings?: React.ComponentType,
|
||||
}
|
||||
|
||||
const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
history.push('/');
|
||||
} else {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
onBackClick={handleBackClick}
|
||||
>
|
||||
<CardTitle title={message} />
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubNavigation;
|
|
@ -44,13 +44,14 @@ const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'def
|
|||
interface ICardHeader {
|
||||
backHref?: string,
|
||||
onBackClick?: (event: React.MouseEvent) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Card header container with back button.
|
||||
* Typically holds a CardTitle.
|
||||
*/
|
||||
const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }): JSX.Element => {
|
||||
const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBackClick }): JSX.Element => {
|
||||
const intl = useIntl();
|
||||
|
||||
const renderBackButton = () => {
|
||||
|
@ -70,7 +71,7 @@ const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }):
|
|||
};
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' space={2} className='mb-4'>
|
||||
<HStack alignItems='center' space={2} className={classNames('mb-4', className)}>
|
||||
{renderBackButton()}
|
||||
|
||||
{children}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import Column from '../column';
|
||||
import { Column } from '../column';
|
||||
|
||||
describe('<Column />', () => {
|
||||
it('renders correctly with minimal props', () => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -6,6 +7,35 @@ import { useSoapboxConfig } from 'soapbox/hooks';
|
|||
|
||||
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
|
||||
|
||||
type IColumnHeader = Pick<IColumn, 'label' | 'backHref' |'transparent'>;
|
||||
|
||||
/** Contains the column title with optional back button. */
|
||||
const ColumnHeader: React.FC<IColumnHeader> = ({ label, backHref, transparent }) => {
|
||||
const history = useHistory();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (backHref) {
|
||||
history.push(backHref);
|
||||
return;
|
||||
}
|
||||
|
||||
if (history.length === 1) {
|
||||
history.push('/');
|
||||
} else {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
className={classNames({ 'px-4 pt-4 sm:p-0': transparent })}
|
||||
onBackClick={handleBackClick}
|
||||
>
|
||||
<CardTitle title={label} />
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IColumn {
|
||||
/** Route the back button goes to. */
|
||||
backHref?: string,
|
||||
|
@ -24,37 +54,8 @@ export interface IColumn {
|
|||
/** A backdrop for the main section of the UI. */
|
||||
const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedRef<HTMLDivElement>): JSX.Element => {
|
||||
const { backHref, children, label, transparent = false, withHeader = true, className } = props;
|
||||
|
||||
const history = useHistory();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (backHref) {
|
||||
history.push(backHref);
|
||||
return;
|
||||
}
|
||||
|
||||
if (history.length === 1) {
|
||||
history.push('/');
|
||||
} else {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
const renderChildren = () => (
|
||||
<Card variant={transparent ? undefined : 'rounded'} className={className}>
|
||||
{withHeader ? (
|
||||
<CardHeader onBackClick={handleBackClick}>
|
||||
<CardTitle title={label} />
|
||||
</CardHeader>
|
||||
) : null}
|
||||
|
||||
<CardBody>
|
||||
{children}
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
|
||||
return (
|
||||
<div role='region' className='relative' ref={ref} aria-label={label} column-type={transparent ? 'transparent' : 'filled'}>
|
||||
<Helmet>
|
||||
|
@ -69,9 +70,20 @@ const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedR
|
|||
)}
|
||||
</Helmet>
|
||||
|
||||
{renderChildren()}
|
||||
<Card variant={transparent ? undefined : 'rounded'} className={className}>
|
||||
{withHeader && (
|
||||
<ColumnHeader label={label} backHref={backHref} transparent={transparent} />
|
||||
)}
|
||||
|
||||
<CardBody>
|
||||
{children}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Column;
|
||||
export {
|
||||
Column,
|
||||
ColumnHeader,
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ export { default as Banner } from './banner/banner';
|
|||
export { default as Button } from './button/button';
|
||||
export { Card, CardBody, CardHeader, CardTitle } from './card/card';
|
||||
export { default as Checkbox } from './checkbox/checkbox';
|
||||
export { default as Column } from './column/column';
|
||||
export { Column, ColumnHeader } from './column/column';
|
||||
export { default as Counter } from './counter/counter';
|
||||
export { default as Datepicker } from './datepicker/datepicker';
|
||||
export { default as Divider } from './divider/divider';
|
||||
|
|
|
@ -3,10 +3,9 @@ import { defineMessages, FormattedDate, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchModerationLog } from 'soapbox/actions/admin';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.moderation_log', defaultMessage: 'Moderation Log' },
|
||||
emptyMessage: { id: 'admin.moderation_log.empty_message', defaultMessage: 'You have not performed any moderation actions yet. When you do, a history will be shown here.' },
|
||||
|
@ -47,7 +46,7 @@ const ModerationLog = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Column icon='balance-scale' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
|
|
|
@ -8,9 +8,9 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { fetchUsers } from 'soapbox/actions/admin';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { SimpleForm, TextInput } from 'soapbox/features/forms';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.admin.users', defaultMessage: 'Users' },
|
||||
|
|
|
@ -4,10 +4,9 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchBackups, createBackup } from 'soapbox/actions/backups';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/better-column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.backups', defaultMessage: 'Backups' },
|
||||
create: { id: 'backups.actions.create', defaultMessage: 'Create backup' },
|
||||
|
@ -52,7 +51,11 @@ const Backups = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<Column icon='cloud-download' label={intl.formatMessage(messages.heading)} menu={makeColumnMenu()}>
|
||||
<Column
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
// @ts-ignore FIXME: make this menu available.
|
||||
menu={makeColumnMenu()}
|
||||
>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
|
|
|
@ -5,7 +5,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'soapbox/actions/bookmarks';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
|
@ -36,10 +35,7 @@ const Bookmarks: React.FC = () => {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column transparent withHeader={false}>
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(messages.heading)} />
|
||||
</div>
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
import { connectCommunityStream } from 'soapbox/actions/streaming';
|
||||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
|
@ -41,11 +40,7 @@ const CommunityTimeline = () => {
|
|||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<div className='px-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||
</div>
|
||||
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
|
|
|
@ -3,8 +3,7 @@ import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
|||
|
||||
import { createApp } from 'soapbox/actions/apps';
|
||||
import { obtainOAuthToken } from 'soapbox/actions/oauth';
|
||||
import { Button, Form, FormActions, FormGroup, Input, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { Column, Button, Form, FormActions, FormGroup, Input, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { getBaseURL } from 'soapbox/utils/accounts';
|
||||
|
||||
|
|
|
@ -4,9 +4,7 @@ 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';
|
||||
import { Column, Button, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers', defaultMessage: 'Developers' },
|
||||
|
|
|
@ -5,12 +5,10 @@ import { Link, useHistory } from 'react-router-dom';
|
|||
|
||||
import { changeSettingImmediate } from 'soapbox/actions/settings';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { Column, Text } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.developers', defaultMessage: 'Developers' },
|
||||
leave: { id: 'developers.leave', defaultMessage: 'You have left developers' },
|
||||
|
|
|
@ -6,11 +6,9 @@ import { useDispatch } from 'react-redux';
|
|||
import { fetchDomainBlocks, expandDomainBlocks } from 'soapbox/actions/domain-blocks';
|
||||
import Domain from 'soapbox/components/domain';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
|
||||
|
@ -42,7 +40,7 @@ const DomainBlocks: React.FC = () => {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
|
||||
|
||||
return (
|
||||
<Column icon='minus-circle' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='domain_blocks'
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
|
|
|
@ -6,8 +6,7 @@ import {
|
|||
exportBlocks,
|
||||
exportMutes,
|
||||
} from 'soapbox/actions/export-data';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import CSVExporter from './components/csv-exporter';
|
||||
|
||||
|
@ -38,7 +37,7 @@ const ExportData = () => {
|
|||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Column icon='cloud-download-alt' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<CSVExporter action={exportFollows} messages={followMessages} />
|
||||
<CSVExporter action={exportBlocks} messages={blockMessages} />
|
||||
<CSVExporter action={exportMutes} messages={muteMessages} />
|
||||
|
|
|
@ -7,11 +7,10 @@ import { fetchAccount, fetchAccountByUsername } from 'soapbox/actions/accounts';
|
|||
import { fetchFavouritedStatuses, expandFavouritedStatuses, fetchAccountFavouritedStatuses, expandAccountFavouritedStatuses } from 'soapbox/actions/favourites';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { findAccountByUsername } from 'soapbox/selectors';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.favourited_statuses', defaultMessage: 'Liked posts' },
|
||||
});
|
||||
|
|
|
@ -2,13 +2,11 @@ import React, { useState, useCallback } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Accordion } from 'soapbox/components/ui';
|
||||
import { Column, Accordion } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useInstance } from 'soapbox/hooks';
|
||||
import { makeGetHosts } from 'soapbox/selectors';
|
||||
import { federationRestrictionsDisclosed } from 'soapbox/utils/state';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
import RestrictedInstance from './components/restricted-instance';
|
||||
|
||||
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
|
@ -39,7 +37,7 @@ const FederationRestrictions = () => {
|
|||
const emptyMessage = disclosed ? messages.emptyMessage : messages.notDisclosed;
|
||||
|
||||
return (
|
||||
<Column icon='gavel' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Accordion
|
||||
headline={intl.formatMessage(messages.boxTitle)}
|
||||
expanded={explanationBoxExpanded}
|
||||
|
|
|
@ -4,9 +4,8 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchSuggestions } from 'soapbox/actions/suggestions';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Stack, Text } from 'soapbox/components/ui';
|
||||
import { Column, Stack, Text } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -5,11 +5,9 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { fetchFollowRequests, expandFollowRequests } from 'soapbox/actions/accounts';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
import AccountAuthorize from './components/account-authorize';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -42,7 +40,7 @@ const FollowRequests: React.FC = () => {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='user-plus' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='follow_requests'
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
|
|
|
@ -11,13 +11,11 @@ import {
|
|||
} from 'soapbox/actions/accounts';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { findAccountByUsername } from 'soapbox/selectors';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.followers', defaultMessage: 'Followers' },
|
||||
});
|
||||
|
|
|
@ -11,13 +11,11 @@ import {
|
|||
} from 'soapbox/actions/accounts';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Spinner } from 'soapbox/components/ui';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { findAccountByUsername } from 'soapbox/selectors';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.following', defaultMessage: 'Following' },
|
||||
});
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { connectHashtagStream } from 'soapbox/actions/streaming';
|
||||
import { expandHashtagTimeline, clearTimeline } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import Timeline from 'soapbox/features/ui/components/timeline';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
@ -15,6 +14,13 @@ type Mode = 'any' | 'all' | 'none';
|
|||
type Tag = { value: string };
|
||||
type Tags = { [k in Mode]: Tag[] };
|
||||
|
||||
const messages = defineMessages({
|
||||
any: { id: 'hashtag.column_header.tag_mode.any', defaultMessage: 'or {additional}' },
|
||||
all: { id: 'hashtag.column_header.tag_mode.all', defaultMessage: 'and {additional}' },
|
||||
none: { id: 'hashtag.column_header.tag_mode.none', defaultMessage: 'without {additional}' },
|
||||
empty: { id: 'empty_column.hashtag', defaultMessage: 'There is nothing in this hashtag yet.' },
|
||||
});
|
||||
|
||||
interface IHashtagTimeline {
|
||||
params?: {
|
||||
id?: string,
|
||||
|
@ -23,6 +29,7 @@ interface IHashtagTimeline {
|
|||
}
|
||||
|
||||
export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
||||
const intl = useIntl();
|
||||
const id = params?.id || '';
|
||||
const tags = params?.tags || { any: [], all: [], none: [] };
|
||||
|
||||
|
@ -31,22 +38,22 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
|
||||
// Mastodon supports displaying results from multiple hashtags.
|
||||
// https://github.com/mastodon/mastodon/issues/6359
|
||||
const title = () => {
|
||||
const title: React.ReactNode[] = [`#${id}`];
|
||||
const title = (): string => {
|
||||
const title: string[] = [`#${id}`];
|
||||
|
||||
if (additionalFor('any')) {
|
||||
title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any' values={{ additional: additionalFor('any') }} defaultMessage='or {additional}' />);
|
||||
title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('any') }));
|
||||
}
|
||||
|
||||
if (additionalFor('all')) {
|
||||
title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all' values={{ additional: additionalFor('all') }} defaultMessage='and {additional}' />);
|
||||
title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('all') }));
|
||||
}
|
||||
|
||||
if (additionalFor('none')) {
|
||||
title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: additionalFor('none') }} defaultMessage='without {additional}' />);
|
||||
title.push(' ', intl.formatMessage(messages.any, { additional: additionalFor('none') }));
|
||||
}
|
||||
|
||||
return title;
|
||||
return title.join('');
|
||||
};
|
||||
|
||||
const additionalFor = (mode: Mode) => {
|
||||
|
@ -98,16 +105,12 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
|
|||
}, [id]);
|
||||
|
||||
return (
|
||||
<Column label={`#${id}`} transparent withHeader={false}>
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={title()} />
|
||||
</div>
|
||||
|
||||
<Column label={title()} transparent>
|
||||
<Timeline
|
||||
scrollKey='hashtag_timeline'
|
||||
timelineId={`hashtag:${id}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
|
||||
emptyMessage={intl.formatMessage(messages.empty)}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
|
|
|
@ -6,8 +6,7 @@ import {
|
|||
importBlocks,
|
||||
importMutes,
|
||||
} from 'soapbox/actions/import-data';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import CSVImporter from './components/csv-importer';
|
||||
|
||||
|
@ -38,7 +37,7 @@ const ImportData = () => {
|
|||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Column icon='cloud-upload-alt' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<CSVImporter action={importFollows} messages={followMessages} />
|
||||
<CSVImporter action={importBlocks} messages={blockMessages} />
|
||||
<CSVImporter action={importMutes} messages={muteMessages} />
|
||||
|
|
|
@ -7,26 +7,16 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import { connectListStream } from 'soapbox/actions/streaming';
|
||||
import { expandListTimeline } from 'soapbox/actions/timelines';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import { Button, Spinner } from 'soapbox/components/ui';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import { Column, Button, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
// const messages = defineMessages({
|
||||
// deleteHeading: { id: 'confirmations.delete_list.heading', defaultMessage: 'Delete list' },
|
||||
// deleteMessage: { id: 'confirmations.delete_list.message', defaultMessage: 'Are you sure you want to permanently delete this list?' },
|
||||
// deleteConfirm: { id: 'confirmations.delete_list.confirm', defaultMessage: 'Delete' },
|
||||
// });
|
||||
|
||||
const ListTimeline: React.FC = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
// const intl = useIntl();
|
||||
// const history = useHistory();
|
||||
|
||||
const list = useAppSelector((state) => state.lists.get(id));
|
||||
// const hasUnread = useAppSelector((state) => state.timelines.get(`list:${props.params.id}`)?.unread > 0);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchList(id));
|
||||
|
@ -47,19 +37,6 @@ const ListTimeline: React.FC = () => {
|
|||
dispatch(openModal('LIST_EDITOR', { listId: id }));
|
||||
};
|
||||
|
||||
// const handleDeleteClick = () => {
|
||||
// dispatch(openModal('CONFIRM', {
|
||||
// icon: require('@tabler/icons/trash.svg'),
|
||||
// heading: intl.formatMessage(messages.deleteHeading),
|
||||
// message: intl.formatMessage(messages.deleteMessage),
|
||||
// confirm: intl.formatMessage(messages.deleteConfirm),
|
||||
// onConfirm: () => {
|
||||
// dispatch(deleteList(id));
|
||||
// history.push('/lists');
|
||||
// },
|
||||
// }));
|
||||
// };
|
||||
|
||||
const title = list ? list.title : id;
|
||||
|
||||
if (typeof list === 'undefined') {
|
||||
|
@ -85,26 +62,7 @@ const ListTimeline: React.FC = () => {
|
|||
);
|
||||
|
||||
return (
|
||||
<Column label={title} heading={title} transparent withHeader={false}>
|
||||
{/* <HomeColumnHeader activeItem='lists' activeSubItem={id} active={hasUnread}>
|
||||
<div className='column-header__links'>
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={handleEditClick}>
|
||||
<Icon id='pencil' /> <FormattedMessage id='lists.edit' defaultMessage='Edit list' />
|
||||
</button>
|
||||
|
||||
<button className='text-btn column-header__setting-btn' tabIndex='0' onClick={handleDeleteClick}>
|
||||
<Icon id='trash' /> <FormattedMessage id='lists.delete' defaultMessage='Delete list' />
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<Link to='/lists' className='text-btn column-header__setting-btn column-header__setting-btn--link'>
|
||||
<FormattedMessage id='lists.view_all' defaultMessage='View all lists' />
|
||||
<Icon id='arrow-right' />
|
||||
</Link>
|
||||
</div>
|
||||
</HomeColumnHeader> */}
|
||||
|
||||
<Column label={title} transparent>
|
||||
<Timeline
|
||||
scrollKey='list_timeline'
|
||||
timelineId={`list:${id}`}
|
||||
|
|
|
@ -5,10 +5,9 @@ import { useParams } from 'react-router-dom';
|
|||
import { fetchPinnedStatuses } from 'soapbox/actions/pin-statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing-indicator';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned posts' },
|
||||
});
|
||||
|
@ -36,7 +35,7 @@ const PinnedStatuses = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent withHeader={false}>
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
|
|
|
@ -6,7 +6,6 @@ import { changeSetting } from 'soapbox/actions/settings';
|
|||
import { connectPublicStream } from 'soapbox/actions/streaming';
|
||||
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Accordion, Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
|
||||
|
||||
|
@ -61,11 +60,7 @@ const CommunityTimeline = () => {
|
|||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<div className='px-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||
</div>
|
||||
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<PinnedHostsPicker />
|
||||
|
||||
{showExplanationBox && <div className='mb-4'>
|
||||
|
|
55
app/soapbox/features/quotes/index.tsx
Normal file
55
app/soapbox/features/quotes/index.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { expandStatusQuotes, fetchStatusQuotes } from 'soapbox/actions/status-quotes';
|
||||
import StatusList from 'soapbox/components/status-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
|
||||
});
|
||||
|
||||
const handleLoadMore = debounce((statusId: string, dispatch: React.Dispatch<any>) =>
|
||||
dispatch(expandStatusQuotes(statusId)), 300, { leading: true });
|
||||
|
||||
const Quotes: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const { statusId } = useParams<{ statusId: string }>();
|
||||
|
||||
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>()));
|
||||
const isLoading = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'isLoading'], true));
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists.getIn([`quotes:${statusId}`, 'next']));
|
||||
|
||||
React.useEffect(() => {
|
||||
dispatch(fetchStatusQuotes(statusId));
|
||||
}, [statusId]);
|
||||
|
||||
const handleRefresh = async() => {
|
||||
await dispatch(fetchStatusQuotes(statusId));
|
||||
};
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.quotes' defaultMessage='This post has not been quoted yet.' />;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
||||
<StatusList
|
||||
statusIds={statusIds as ImmutableOrderedSet<string>}
|
||||
scrollKey={`quotes:${statusId}`}
|
||||
hasMore={hasMore}
|
||||
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
||||
onLoadMore={() => handleLoadMore(statusId, dispatch)}
|
||||
onRefresh={handleRefresh}
|
||||
emptyMessage={emptyMessage}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Quotes;
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { connectRemoteStream } from 'soapbox/actions/streaming';
|
||||
import { expandRemoteTimeline } from 'soapbox/actions/timelines';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Column, HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
|
@ -13,10 +12,6 @@ import Timeline from '../ui/components/timeline';
|
|||
|
||||
import PinnedHostsPicker from './components/pinned-hosts-picker';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.remote', defaultMessage: 'Federated timeline' },
|
||||
});
|
||||
|
||||
interface IRemoteTimeline {
|
||||
params?: {
|
||||
instance?: string,
|
||||
|
@ -25,7 +20,6 @@ interface IRemoteTimeline {
|
|||
|
||||
/** View statuses from a remote instance. */
|
||||
const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
|
@ -65,25 +59,21 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
|||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent withHeader={false}>
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={instance} />
|
||||
<Column label={instance} transparent>
|
||||
{instance && <PinnedHostsPicker host={instance} />}
|
||||
|
||||
{instance && <PinnedHostsPicker host={instance} />}
|
||||
|
||||
{!pinned && (
|
||||
<HStack className='mb-4 px-2' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleCloseClick} />
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='remote_timeline.filter_message'
|
||||
defaultMessage='You are viewing the timeline of {instance}.'
|
||||
values={{ instance }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
{!pinned && (
|
||||
<HStack className='mb-4 px-2' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleCloseClick} />
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='remote_timeline.filter_message'
|
||||
defaultMessage='You are viewing the timeline of {instance}.'
|
||||
values={{ instance }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_${instance}_timeline`}
|
||||
|
|
|
@ -4,10 +4,9 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchScheduledStatuses, expandScheduledStatuses } from 'soapbox/actions/scheduled-statuses';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
import ScheduledStatus from './components/scheduled-status';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -33,7 +32,7 @@ const ScheduledStatuses = () => {
|
|||
const emptyMessage = <FormattedMessage id='empty_column.scheduled_statuses' defaultMessage="You don't have any scheduled statuses yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column icon='calendar' label={intl.formatMessage(messages.heading)}>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='scheduled_statuses'
|
||||
hasMore={hasMore}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { List as ImmutableList } from 'immutable';
|
|||
import React from 'react';
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { HStack, Text, Emoji } from 'soapbox/components/ui';
|
||||
|
@ -16,6 +17,8 @@ interface IStatusInteractionBar {
|
|||
}
|
||||
|
||||
const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.Element | null => {
|
||||
const history = useHistory();
|
||||
|
||||
const me = useAppSelector(({ me }) => me);
|
||||
const { allowedEmoji } = useSoapboxConfig();
|
||||
const dispatch = useDispatch();
|
||||
|
@ -81,6 +84,28 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
return null;
|
||||
};
|
||||
|
||||
const navigateToQuotes: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`);
|
||||
};
|
||||
|
||||
const getQuotes = () => {
|
||||
if (status.quotes_count) {
|
||||
return (
|
||||
<InteractionCounter count={status.quotes_count} onClick={navigateToQuotes}>
|
||||
<FormattedMessage
|
||||
id='status.interactions.quotes'
|
||||
defaultMessage='{count, plural, one {Quote} other {Quotes}}'
|
||||
values={{ count: status.quotes_count }}
|
||||
/>
|
||||
</InteractionCounter>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleOpenFavouritesModal: React.EventHandler<React.MouseEvent<HTMLButtonElement>> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
|
@ -142,6 +167,7 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
|
|||
return (
|
||||
<HStack space={3}>
|
||||
{getReposts()}
|
||||
{getQuotes()}
|
||||
{features.emojiReacts ? getEmojiReacts() : getFavourites()}
|
||||
</HStack>
|
||||
);
|
||||
|
|
|
@ -29,7 +29,6 @@ import MissingIndicator from 'soapbox/components/missing-indicator';
|
|||
import PullToRefresh from 'soapbox/components/pull-to-refresh';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import StatusActionBar from 'soapbox/components/status-action-bar';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import Tombstone from 'soapbox/components/tombstone';
|
||||
import { Column, Stack } from 'soapbox/components/ui';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status';
|
||||
|
@ -516,11 +515,7 @@ const Thread: React.FC<IThread> = (props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(titleMessage, { username })} transparent withHeader={false}>
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={intl.formatMessage(titleMessage, { username })} />
|
||||
</div>
|
||||
|
||||
<Column label={intl.formatMessage(titleMessage, { username })} transparent>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<Stack space={2}>
|
||||
<div ref={node} className='thread'>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { importFetchedStatuses } from 'soapbox/actions/importer';
|
||||
import { expandTimelineSuccess } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
|
||||
import { Column } from '../../components/ui';
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
@ -40,8 +39,7 @@ const TestTimeline: React.FC = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} />
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown-menu-container';
|
||||
|
||||
import ColumnHeader from './column-header';
|
||||
|
||||
// Yes, there are 3 types of columns at this point, but this one is better, I swear
|
||||
export default class BetterColumn extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
heading: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
active: PropTypes.bool,
|
||||
menu: PropTypes.array,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { heading, icon, children, active, menu, ...rest } = this.props;
|
||||
const columnHeaderId = heading && heading.replace(/ /g, '-');
|
||||
|
||||
return (
|
||||
<Column aria-labelledby={columnHeaderId} className='column--better' {...rest}>
|
||||
<div className='column__top'>
|
||||
{heading && <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />}
|
||||
{menu && (
|
||||
<div className='column__menu'>
|
||||
<DropdownMenu items={menu} icon='ellipsis-v' size={18} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{children}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Column from './column';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column_forbidden.title', defaultMessage: 'Forbidden' },
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
// import classNames from 'clsx';
|
||||
// import Icon from 'soapbox/components/icon';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
|
||||
interface IColumnHeader {
|
||||
icon?: string,
|
||||
type: string
|
||||
active?: boolean,
|
||||
columnHeaderId?: string,
|
||||
}
|
||||
|
||||
const ColumnHeader: React.FC<IColumnHeader> = ({ type }) => {
|
||||
return <SubNavigation message={type} />;
|
||||
};
|
||||
|
||||
export default ColumnHeader;
|
||||
|
||||
// export default class ColumnHeader extends React.PureComponent {
|
||||
|
||||
// static propTypes = {
|
||||
// icon: PropTypes.string,
|
||||
// type: PropTypes.string,
|
||||
// active: PropTypes.bool,
|
||||
// onClick: PropTypes.func,
|
||||
// columnHeaderId: PropTypes.string,
|
||||
// };
|
||||
|
||||
// handleClick = () => {
|
||||
// this.props.onClick();
|
||||
// }
|
||||
|
||||
// render() {
|
||||
// const { icon, type, active, columnHeaderId } = this.props;
|
||||
|
||||
// return (
|
||||
// <h1 className={classNames('column-header', { active })} id={columnHeaderId || null}>
|
||||
// <button onClick={this.handleClick}>
|
||||
// {icon && <Icon id={icon} fixedWidth className='column-header__icon' />}
|
||||
// {type}
|
||||
// </button>
|
||||
// </h1>
|
||||
// );
|
||||
// }
|
||||
|
||||
// }
|
|
@ -1,36 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import Pullable from 'soapbox/components/pullable';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import ColumnHeader from './column-header';
|
||||
|
||||
import type { IColumn } from 'soapbox/components/ui/column/column';
|
||||
|
||||
interface IUIColumn extends IColumn {
|
||||
heading?: string,
|
||||
icon?: string,
|
||||
active?: boolean,
|
||||
}
|
||||
|
||||
const UIColumn: React.FC<IUIColumn> = ({
|
||||
heading,
|
||||
icon,
|
||||
children,
|
||||
active,
|
||||
...rest
|
||||
}) => {
|
||||
const columnHeaderId = heading && heading.replace(/ /g, '-');
|
||||
|
||||
return (
|
||||
<Column aria-labelledby={columnHeaderId} {...rest}>
|
||||
{heading && <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} />}
|
||||
<Pullable>
|
||||
{children}
|
||||
</Pullable>
|
||||
</Column>
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
export default UIColumn;
|
|
@ -12,6 +12,7 @@ import { fetchAnnouncements } from 'soapbox/actions/announcements';
|
|||
import { fetchChats } from 'soapbox/actions/chats';
|
||||
import { uploadCompose, resetCompose } from 'soapbox/actions/compose';
|
||||
import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis';
|
||||
import { uploadEventBanner } from 'soapbox/actions/events';
|
||||
import { fetchFilters } from 'soapbox/actions/filters';
|
||||
import { fetchMarker } from 'soapbox/actions/markers';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
|
@ -110,9 +111,10 @@ import {
|
|||
TestTimeline,
|
||||
LogoutPage,
|
||||
AuthTokenList,
|
||||
Quotes,
|
||||
ServiceWorkerInfo,
|
||||
EventInformation,
|
||||
EventDiscussion,
|
||||
ServiceWorkerInfo,
|
||||
Events,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -120,7 +122,6 @@ import { WrappedRoute } from './util/react-router-helpers';
|
|||
// Dummy import, to make sure that <Status /> ends up in the application bundle.
|
||||
// Without this it ends up in ~8 very commonly used bundles.
|
||||
import 'soapbox/components/status';
|
||||
import { uploadEventBanner } from 'soapbox/actions/events';
|
||||
|
||||
const EmptyPage = HomePage;
|
||||
|
||||
|
@ -244,7 +245,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/tags/:id' publicRoute page={DefaultPage} component={HashtagTimeline} content={children} />
|
||||
|
||||
{features.lists && <WrappedRoute path='/lists' page={DefaultPage} component={Lists} content={children} />}
|
||||
{features.lists && <WrappedRoute path='/list/:id' page={HomePage} component={ListTimeline} content={children} />}
|
||||
{features.lists && <WrappedRoute path='/list/:id' page={DefaultPage} component={ListTimeline} content={children} />}
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks' page={DefaultPage} component={Bookmarks} content={children} />}
|
||||
|
||||
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
||||
|
@ -271,6 +272,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/@:username/favorites' component={FavouritedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/quotes' publicRoute page={StatusPage} component={Quotes} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId' publicRoute exact page={EventPage} component={EventInformation} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
|
|
@ -506,6 +506,10 @@ export function AnnouncementsPanel() {
|
|||
return import(/* webpackChunkName: "features/announcements" */'../../../components/announcements/announcements-panel');
|
||||
}
|
||||
|
||||
export function Quotes() {
|
||||
return import(/*webpackChunkName: "features/quotes" */'../../quotes');
|
||||
}
|
||||
|
||||
export function ComposeEventModal() {
|
||||
return import(/* webpackChunkName: "features/compose_event_modal" */'../components/modals/compose-event-modal/compose-event-modal');
|
||||
}
|
||||
|
|
|
@ -60,6 +60,7 @@ export const StatusRecord = ImmutableRecord({
|
|||
pleroma: ImmutableMap<string, any>(),
|
||||
poll: null as EmbeddedEntity<Poll>,
|
||||
quote: null as EmbeddedEntity<any>,
|
||||
quotes_count: 0,
|
||||
reblog: null as EmbeddedEntity<any>,
|
||||
reblogged: false,
|
||||
reblogs_count: 0,
|
||||
|
@ -158,6 +159,8 @@ const fixQuote = (status: ImmutableMap<string, any>) => {
|
|||
return status.withMutations(status => {
|
||||
status.update('quote', quote => quote || status.getIn(['pleroma', 'quote']) || null);
|
||||
status.deleteIn(['pleroma', 'quote']);
|
||||
status.update('quotes_count', quotes_count => quotes_count || status.getIn(['pleroma', 'quotes_count'], 0));
|
||||
status.deleteIn(['pleroma', 'quotes_count']);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -4,6 +4,15 @@ import {
|
|||
Record as ImmutableRecord,
|
||||
} from 'immutable';
|
||||
|
||||
import {
|
||||
STATUS_QUOTES_EXPAND_FAIL,
|
||||
STATUS_QUOTES_EXPAND_REQUEST,
|
||||
STATUS_QUOTES_EXPAND_SUCCESS,
|
||||
STATUS_QUOTES_FETCH_FAIL,
|
||||
STATUS_QUOTES_FETCH_REQUEST,
|
||||
STATUS_QUOTES_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/status-quotes';
|
||||
|
||||
import {
|
||||
BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||
BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||
|
@ -59,7 +68,7 @@ import {
|
|||
import type { AnyAction } from 'redux';
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const StatusListRecord = ImmutableRecord({
|
||||
export const StatusListRecord = ImmutableRecord({
|
||||
next: null as string | null,
|
||||
loaded: false,
|
||||
isLoading: null as boolean | null,
|
||||
|
@ -178,6 +187,16 @@ export default function statusLists(state = initialState, action: AnyAction) {
|
|||
case SCHEDULED_STATUS_CANCEL_REQUEST:
|
||||
case SCHEDULED_STATUS_CANCEL_SUCCESS:
|
||||
return removeOneFromList(state, 'scheduled_statuses', action.id || action.status.id);
|
||||
case STATUS_QUOTES_FETCH_REQUEST:
|
||||
case STATUS_QUOTES_EXPAND_REQUEST:
|
||||
return setLoading(state, `quotes:${action.statusId}`, true);
|
||||
case STATUS_QUOTES_FETCH_FAIL:
|
||||
case STATUS_QUOTES_EXPAND_FAIL:
|
||||
return setLoading(state, `quotes:${action.statusId}`, false);
|
||||
case STATUS_QUOTES_FETCH_SUCCESS:
|
||||
return normalizeList(state, `quotes:${action.statusId}`, action.statuses, action.next);
|
||||
case STATUS_QUOTES_EXPAND_SUCCESS:
|
||||
return appendToList(state, `quotes:${action.statusId}`, action.statuses, action.next);
|
||||
case RECENT_EVENTS_FETCH_REQUEST:
|
||||
return setLoading(state, 'recent_events', true);
|
||||
case RECENT_EVENTS_FETCH_FAIL:
|
||||
|
|
Loading…
Reference in a new issue