Merge branch 'ts' into 'develop'
TypeScript, FC (reducers, search) See merge request soapbox-pub/soapbox-fe!1502
This commit is contained in:
commit
02a65608ba
|
@ -1,8 +1,9 @@
|
|||
import { jest } from '@jest/globals';
|
||||
import { AxiosInstance, AxiosResponse } from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
import type { AxiosInstance, AxiosResponse } from 'axios';
|
||||
|
||||
const api = jest.requireActual('../api') as Record<string, Function>;
|
||||
let mocks: Array<Function> = [];
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AnyAction } from 'redux';
|
||||
|
||||
import { staticClient } from '../api';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const FETCH_ABOUT_PAGE_REQUEST = 'FETCH_ABOUT_PAGE_REQUEST';
|
||||
const FETCH_ABOUT_PAGE_SUCCESS = 'FETCH_ABOUT_PAGE_SUCCESS';
|
||||
const FETCH_ABOUT_PAGE_FAIL = 'FETCH_ABOUT_PAGE_FAIL';
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import api from '../api';
|
||||
|
||||
import { openModal, closeModal } from './modals';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
const ACCOUNT_NOTE_SUBMIT_REQUEST = 'ACCOUNT_NOTE_SUBMIT_REQUEST';
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { AxiosError } from 'axios';
|
||||
import { defineMessages, MessageDescriptor } from 'react-intl';
|
||||
|
||||
import { httpErrorMessages } from 'soapbox/utils/errors';
|
||||
|
||||
import { SnackbarActionSeverity } from './snackbar';
|
||||
import type { SnackbarActionSeverity } from './snackbar';
|
||||
import type { AnyAction } from '@reduxjs/toolkit';
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { AnyAction } from '@reduxjs/toolkit';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||
import { getNextLinkName } from 'soapbox/utils/quirks';
|
||||
|
||||
|
@ -9,6 +6,9 @@ import api, { getLinks } from '../api';
|
|||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedAccounts } from './importer';
|
||||
|
||||
import type { AnyAction } from '@reduxjs/toolkit';
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
|
||||
const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
|
||||
const BLOCKS_FETCH_FAIL = 'BLOCKS_FETCH_FAIL';
|
||||
|
|
|
@ -27,7 +27,6 @@ export const submit = (routerHistory) => (dispatch, getState) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
export const create = (title, description, coverImage, routerHistory) => (dispatch, getState) => {
|
||||
if (!isLoggedIn(getState)) return;
|
||||
|
||||
|
@ -47,7 +46,6 @@ export const create = (title, description, coverImage, routerHistory) => (dispat
|
|||
}).catch(err => dispatch(createFail(err)));
|
||||
};
|
||||
|
||||
|
||||
export const createRequest = id => ({
|
||||
type: GROUP_CREATE_REQUEST,
|
||||
id,
|
||||
|
@ -82,7 +80,6 @@ export const update = (groupId, title, description, coverImage, routerHistory) =
|
|||
}).catch(err => dispatch(updateFail(err)));
|
||||
};
|
||||
|
||||
|
||||
export const updateRequest = id => ({
|
||||
type: GROUP_UPDATE_REQUEST,
|
||||
id,
|
||||
|
|
|
@ -23,9 +23,11 @@ export interface MenuItem {
|
|||
to?: string,
|
||||
newTab?: boolean,
|
||||
isLogout?: boolean,
|
||||
icon: string,
|
||||
icon?: string,
|
||||
count?: number,
|
||||
destructive?: boolean,
|
||||
meta?: string,
|
||||
active?: boolean,
|
||||
}
|
||||
|
||||
export type Menu = Array<MenuItem | null>;
|
||||
|
|
|
@ -10,24 +10,24 @@ import { shortNumberFormat } from '../utils/numbers';
|
|||
import Permalink from './permalink';
|
||||
import { HStack, Stack, Text } from './ui';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import type { Tag } from 'soapbox/types/entities';
|
||||
|
||||
interface IHashtag {
|
||||
hashtag: ImmutableMap<string, any>,
|
||||
hashtag: Tag,
|
||||
}
|
||||
|
||||
const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
||||
const count = Number(hashtag.getIn(['history', 0, 'accounts']));
|
||||
const brandColor = useSelector((state) => getSoapboxConfig(state).get('brandColor'));
|
||||
const count = Number(hashtag.history?.get(0)?.accounts);
|
||||
const brandColor = useSelector((state) => getSoapboxConfig(state).brandColor);
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' justifyContent='between' data-testid='hashtag'>
|
||||
<Stack>
|
||||
<Permalink href={hashtag.get('url')} to={`/tags/${hashtag.get('name')}`} className='hover:underline'>
|
||||
<Text tag='span' size='sm' weight='semibold'>#{hashtag.get('name')}</Text>
|
||||
<Permalink href={hashtag.url} to={`/tags/${hashtag.name}`} className='hover:underline'>
|
||||
<Text tag='span' size='sm' weight='semibold'>#{hashtag.name}</Text>
|
||||
</Permalink>
|
||||
|
||||
{hashtag.get('history') && (
|
||||
{hashtag.history && (
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage
|
||||
id='trends.count_by_accounts'
|
||||
|
@ -41,12 +41,12 @@ const Hashtag: React.FC<IHashtag> = ({ hashtag }) => {
|
|||
)}
|
||||
</Stack>
|
||||
|
||||
{hashtag.get('history') && (
|
||||
{hashtag.history && (
|
||||
<div className='w-[40px]' data-testid='sparklines'>
|
||||
<Sparklines
|
||||
width={40}
|
||||
height={28}
|
||||
data={hashtag.get('history').reverse().map((day: ImmutableMap<string, any>) => day.get('uses')).toArray()}
|
||||
data={hashtag.history.reverse().map((day) => +day.uses).toArray()}
|
||||
>
|
||||
<SparklinesCurve style={{ fill: 'none' }} color={brandColor} />
|
||||
</Sparklines>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import { Button } from 'soapbox/components/ui';
|
||||
|
||||
interface ILoadMore {
|
||||
onClick: () => void,
|
||||
onClick: React.MouseEventHandler,
|
||||
disabled?: boolean,
|
||||
visible?: Boolean,
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@ class ModalRoot extends React.PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
const focusable = Array.from(this.node.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none');
|
||||
|
|
|
@ -29,7 +29,6 @@ const MoreFollows: React.FC<IMoreFollows> = ({ visible = true, count, type }) =>
|
|||
return intl.formatMessage(messages[type], { count });
|
||||
};
|
||||
|
||||
|
||||
// If the instance isn't federating, there are no remote followers
|
||||
if (!features.federating) {
|
||||
return null;
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import PullToRefresh from './pull-to-refresh';
|
||||
|
||||
interface IPullable {
|
||||
children: JSX.Element,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -34,7 +34,6 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div data-testid='still-image-container' className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
||||
<img src={src} alt={alt} ref={img} onLoad={handleImageLoad} />
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useSoapboxConfig } from 'soapbox/hooks';
|
|||
|
||||
import { Card, CardBody, CardHeader, CardTitle } from '../card/card';
|
||||
|
||||
interface IColumn {
|
||||
export interface IColumn {
|
||||
/** Route the back button goes to. */
|
||||
backHref?: string,
|
||||
/** Column title text. */
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { DropdownPlacement, IDropdown } from 'soapbox/components/dropdown_m
|
|||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
isModalOpen: Boolean(state.modals.size && state.modals.last().modalType === 'ACTIONS'),
|
||||
isModalOpen: Boolean(state.modals.size && state.modals.last()!.modalType === 'ACTIONS'),
|
||||
dropdownPlacement: state.dropdown_menu.placement,
|
||||
openDropdownId: state.dropdown_menu.openId,
|
||||
openedViaKeyboard: state.dropdown_menu.keyboard,
|
||||
|
|
|
@ -1,220 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
} from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { expandAccountMediaTimeline } from 'soapbox/actions/timelines';
|
||||
import LoadMore from 'soapbox/components/load_more';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import MediaItem from './components/media_item';
|
||||
|
||||
const mapStateToProps = (state, { params, withReplies = false }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const accountFetchError = ((state.getIn(['accounts', -1, 'username']) || '').toLowerCase() === username.toLowerCase());
|
||||
const features = getFeatures(state.get('instance'));
|
||||
|
||||
let accountId = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
const account = findAccountByUsername(state, username);
|
||||
accountId = account ? account.getIn(['id'], null) : -1;
|
||||
accountUsername = account ? account.getIn(['acct'], '') : '';
|
||||
}
|
||||
|
||||
const isBlocked = state.getIn(['relationships', accountId, 'blocked_by'], false);
|
||||
const unavailable = (me === accountId) ? false : (isBlocked && !features.blockersVisible);
|
||||
|
||||
return {
|
||||
accountId,
|
||||
unavailable,
|
||||
accountUsername,
|
||||
isAccount: !!state.getIn(['accounts', accountId]),
|
||||
attachments: getAccountGallery(state, accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${accountId}:media`, 'hasMore']),
|
||||
};
|
||||
};
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
maxId: PropTypes.string,
|
||||
onLoadMore: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLoadMore = () => {
|
||||
this.props.onLoadMore(this.props.maxId);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<LoadMore
|
||||
disabled={this.props.disabled}
|
||||
onClick={this.handleLoadMore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class AccountGallery extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
attachments: ImmutablePropTypes.list.isRequired,
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
width: 323,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { params: { username }, accountId } = this.props;
|
||||
|
||||
if (accountId && accountId !== -1) {
|
||||
this.props.dispatch(fetchAccount(accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(accountId));
|
||||
} else {
|
||||
this.props.dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const { accountId, params } = this.props;
|
||||
if (accountId && accountId !== -1 && (accountId !== prevProps.accountId && accountId)) {
|
||||
this.props.dispatch(fetchAccount(params.accountId));
|
||||
this.props.dispatch(expandAccountMediaTimeline(accountId));
|
||||
}
|
||||
}
|
||||
|
||||
handleScrollToBottom = () => {
|
||||
if (this.props.hasMore) {
|
||||
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
||||
const offset = scrollHeight - scrollTop - clientHeight;
|
||||
|
||||
if (150 > offset && !this.props.isLoading) {
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
if (this.props.accountId && this.props.accountId !== -1) {
|
||||
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
handleLoadOlder = e => {
|
||||
e.preventDefault();
|
||||
this.handleScrollToBottom();
|
||||
}
|
||||
|
||||
handleOpenMedia = attachment => {
|
||||
if (attachment.get('type') === 'video') {
|
||||
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status'), account: attachment.get('account') }));
|
||||
} else {
|
||||
const media = attachment.getIn(['status', 'media_attachments']);
|
||||
const index = media.findIndex(x => x.get('id') === attachment.get('id'));
|
||||
|
||||
this.props.dispatch(openModal('MEDIA', { media, index, status: attachment.get('status'), account: attachment.get('account') }));
|
||||
}
|
||||
}
|
||||
|
||||
handleRef = c => {
|
||||
if (c) {
|
||||
this.setState({ width: c.offsetWidth });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { attachments, isLoading, hasMore, isAccount, accountId, unavailable, accountUsername } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId === -1 || (!attachments && isLoading)) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={this.handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={`@${accountUsername}`} transparent withHeader={false}>
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem
|
||||
key={`${attachment.getIn(['status', 'id'])}+${attachment.get('id')}`}
|
||||
attachment={attachment}
|
||||
displayWidth={width}
|
||||
onOpenMedia={this.handleOpenMedia}
|
||||
/>
|
||||
))}
|
||||
|
||||
{
|
||||
attachments.size === 0 &&
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='slist__append'>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
fetchAccount,
|
||||
fetchAccountByUsername,
|
||||
} from 'soapbox/actions/accounts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { expandAccountMediaTimeline } from 'soapbox/actions/timelines';
|
||||
import LoadMore from 'soapbox/components/load_more';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import { Column, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { getAccountGallery, findAccountByUsername } from 'soapbox/selectors';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import MediaItem from './components/media_item';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { Attachment, Status } from 'soapbox/types/entities';
|
||||
|
||||
interface ILoadMoreMedia {
|
||||
maxId: string | null,
|
||||
onLoadMore: (value: string | null) => void,
|
||||
}
|
||||
|
||||
const LoadMoreMedia: React.FC<ILoadMoreMedia> = ({ maxId, onLoadMore }) => {
|
||||
const handleLoadMore = () => {
|
||||
onLoadMore(maxId);
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadMore onClick={handleLoadMore} />
|
||||
);
|
||||
};
|
||||
|
||||
const AccountGallery = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { username } = useParams<{ username: string }>();
|
||||
|
||||
const { accountId, unavailable, accountUsername } = useAppSelector((state) => {
|
||||
const me = state.me;
|
||||
const accountFetchError = (state.accounts.get(-1)?.username || '').toLowerCase() === username.toLowerCase();
|
||||
const features = getFeatures(state.instance);
|
||||
|
||||
let accountId: string | number | null = -1;
|
||||
let accountUsername = username;
|
||||
if (accountFetchError) {
|
||||
accountId = null;
|
||||
} else {
|
||||
const account = findAccountByUsername(state, username);
|
||||
accountId = account ? (account.id || null) : -1;
|
||||
accountUsername = account?.acct || '';
|
||||
}
|
||||
|
||||
const isBlocked = state.relationships.get(String(accountId))?.blocked_by || false;
|
||||
return {
|
||||
accountId,
|
||||
unavailable: (me === accountId) ? false : (isBlocked && !features.blockersVisible),
|
||||
accountUsername,
|
||||
};
|
||||
});
|
||||
const isAccount = useAppSelector((state) => !!state.accounts.get(accountId));
|
||||
const attachments: ImmutableList<Attachment> = useAppSelector((state) => getAccountGallery(state, accountId as string));
|
||||
const isLoading = useAppSelector((state) => state.timelines.getIn([`account:${accountId}:media`, 'isLoading']));
|
||||
const hasMore = useAppSelector((state) => state.timelines.getIn([`account:${accountId}:media`, 'hasMore']));
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [width] = useState(323);
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
if (hasMore) {
|
||||
handleLoadMore(attachments.size > 0 ? attachments.last()!.status.id : undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = (maxId: string | null) => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadOlder: React.MouseEventHandler = e => {
|
||||
e.preventDefault();
|
||||
handleScrollToBottom();
|
||||
};
|
||||
|
||||
const handleOpenMedia = (attachment: Attachment) => {
|
||||
if (attachment.type === 'video') {
|
||||
dispatch(openModal('VIDEO', { media: attachment, status: attachment.status, account: attachment.account }));
|
||||
} else {
|
||||
const media = (attachment.status as Status).media_attachments;
|
||||
const index = media.findIndex((x) => x.id === attachment.id);
|
||||
|
||||
dispatch(openModal('MEDIA', { media, index, status: attachment.status, account: attachment.account }));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(fetchAccount(accountId));
|
||||
dispatch(expandAccountMediaTimeline(accountId));
|
||||
} else {
|
||||
dispatch(fetchAccountByUsername(username));
|
||||
}
|
||||
}, [accountId]);
|
||||
|
||||
if (!isAccount && accountId !== -1) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (accountId === -1 || (!attachments && isLoading)) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isLoading && attachments.size === 0)) {
|
||||
loadOlder = <LoadMore visible={!isLoading} onClick={handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (unavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={`@${accountUsername}`} transparent withHeader={false}>
|
||||
<div role='feed' className='account-gallery__container' ref={ref}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.get(index + 1)?.id} maxId={index > 0 ? (attachments.get(index - 1)?.id || null) : null} onLoadMore={handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem
|
||||
key={`${attachment.status.id}+${attachment.id}`}
|
||||
attachment={attachment}
|
||||
displayWidth={width}
|
||||
onOpenMedia={handleOpenMedia}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!isLoading && attachments.size === 0 && (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='slist__append'>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountGallery;
|
|
@ -8,25 +8,30 @@ The above copyright notice and this permission notice shall be included in all c
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
const hex2rgba = (hex, alpha = 1) => {
|
||||
const [r, g, b] = hex.match(/\w\w/g).map(x => parseInt(x, 16));
|
||||
const hex2rgba = (hex: string, alpha = 1) => {
|
||||
const [r, g, b] = hex.match(/\w\w/g)!.map(x => parseInt(x, 16));
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
export default class Visualizer {
|
||||
|
||||
constructor(tickSize) {
|
||||
tickSize: number
|
||||
canvas?: HTMLCanvasElement
|
||||
context?: CanvasRenderingContext2D
|
||||
analyser?: AnalyserNode
|
||||
|
||||
constructor(tickSize: number) {
|
||||
this.tickSize = tickSize;
|
||||
}
|
||||
|
||||
setCanvas(canvas) {
|
||||
setCanvas(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
if (canvas) {
|
||||
this.context = canvas.getContext('2d');
|
||||
this.context = canvas.getContext('2d')!;
|
||||
}
|
||||
}
|
||||
|
||||
setAudioContext(context, source) {
|
||||
setAudioContext(context: AudioContext, source: MediaElementAudioSourceNode) {
|
||||
const analyser = context.createAnalyser();
|
||||
|
||||
analyser.smoothingTimeConstant = 0.6;
|
||||
|
@ -37,7 +42,7 @@ export default class Visualizer {
|
|||
this.analyser = analyser;
|
||||
}
|
||||
|
||||
getTickPoints(count) {
|
||||
getTickPoints(count: number) {
|
||||
const coords = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
|
@ -48,13 +53,13 @@ export default class Visualizer {
|
|||
return coords;
|
||||
}
|
||||
|
||||
drawTick(cx, cy, mainColor, x1, y1, x2, y2) {
|
||||
drawTick(cx: number, cy: number, mainColor: string, x1: number, y1: number, x2: number, y2: number) {
|
||||
const dx1 = Math.ceil(cx + x1);
|
||||
const dy1 = Math.ceil(cy + y1);
|
||||
const dx2 = Math.ceil(cx + x2);
|
||||
const dy2 = Math.ceil(cy + y2);
|
||||
|
||||
const gradient = this.context.createLinearGradient(dx1, dy1, dx2, dy2);
|
||||
const gradient = this.context!.createLinearGradient(dx1, dy1, dx2, dy2);
|
||||
|
||||
const lastColor = hex2rgba(mainColor, 0);
|
||||
|
||||
|
@ -62,21 +67,21 @@ export default class Visualizer {
|
|||
gradient.addColorStop(0.6, mainColor);
|
||||
gradient.addColorStop(1, lastColor);
|
||||
|
||||
this.context.beginPath();
|
||||
this.context.strokeStyle = gradient;
|
||||
this.context.lineWidth = 2;
|
||||
this.context.moveTo(dx1, dy1);
|
||||
this.context.lineTo(dx2, dy2);
|
||||
this.context.stroke();
|
||||
this.context!.beginPath();
|
||||
this.context!.strokeStyle = gradient;
|
||||
this.context!.lineWidth = 2;
|
||||
this.context!.moveTo(dx1, dy1);
|
||||
this.context!.lineTo(dx2, dy2);
|
||||
this.context!.stroke();
|
||||
}
|
||||
|
||||
getTicks(count, size, radius, scaleCoefficient) {
|
||||
getTicks(count: number, size: number, radius: number, scaleCoefficient: number) {
|
||||
const ticks = this.getTickPoints(count);
|
||||
const lesser = 200;
|
||||
const m = [];
|
||||
const m: Array<Record<'x1' | 'y1' | 'x2' | 'y2', number>> = [];
|
||||
const bufferLength = this.analyser ? this.analyser.frequencyBinCount : 0;
|
||||
const frequencyData = new Uint8Array(bufferLength);
|
||||
const allScales = [];
|
||||
const allScales: Array<number> = [];
|
||||
|
||||
if (this.analyser) {
|
||||
this.analyser.getByteFrequencyData(frequencyData);
|
||||
|
@ -117,20 +122,20 @@ export default class Visualizer {
|
|||
}));
|
||||
}
|
||||
|
||||
clear(width, height) {
|
||||
this.context.clearRect(0, 0, width, height);
|
||||
clear(width: number, height: number) {
|
||||
this.context!.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
draw(cx, cy, color, radius, coefficient) {
|
||||
this.context.save();
|
||||
draw(cx: number, cy: number, color: string, radius: number, coefficient: number) {
|
||||
this.context!.save();
|
||||
|
||||
const ticks = this.getTicks(parseInt(360 * coefficient), this.tickSize, radius, coefficient);
|
||||
const ticks = this.getTicks(parseInt(360 * coefficient as any), this.tickSize, radius, coefficient);
|
||||
|
||||
ticks.forEach(tick => {
|
||||
this.drawTick(cx, cy, color, tick.x1, tick.y1, tick.x2, tick.y2);
|
||||
});
|
||||
|
||||
this.context.restore();
|
||||
this.context!.restore();
|
||||
}
|
||||
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
import LoginForm from './login_form';
|
||||
import OtpAuthForm from './otp_auth_form';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
me: state.get('me'),
|
||||
isLoading: false,
|
||||
standalone: isStandalone(state),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class LoginPage extends ImmutablePureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
}
|
||||
|
||||
state = {
|
||||
isLoading: false,
|
||||
mfa_auth_needed: false,
|
||||
mfa_token: '',
|
||||
shouldRedirect: false,
|
||||
}
|
||||
|
||||
getFormData = (form) => {
|
||||
return Object.fromEntries(
|
||||
Array.from(form).map(i => [i.name, i.value]),
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const token = new URLSearchParams(window.location.search).get('token');
|
||||
|
||||
if (token) {
|
||||
this.setState({ mfa_token: token, mfa_auth_needed: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
const { dispatch, intl, me } = this.props;
|
||||
const { username, password } = this.getFormData(event.target);
|
||||
dispatch(logIn(intl, username, password)).then(({ access_token }) => {
|
||||
return dispatch(verifyCredentials(access_token))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()));
|
||||
}).then(account => {
|
||||
dispatch(closeModal());
|
||||
this.setState({ shouldRedirect: true });
|
||||
if (typeof me === 'string') {
|
||||
dispatch(switchAccount(account.id));
|
||||
}
|
||||
}).catch(error => {
|
||||
const data = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
this.setState({ mfa_auth_needed: true, mfa_token: data.mfa_token });
|
||||
}
|
||||
this.setState({ isLoading: false });
|
||||
});
|
||||
this.setState({ isLoading: true });
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { standalone } = this.props;
|
||||
const { isLoading, mfa_auth_needed, mfa_token, shouldRedirect } = this.state;
|
||||
|
||||
if (standalone) return <Redirect to='/login/external' />;
|
||||
|
||||
if (shouldRedirect) {
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
if (mfa_auth_needed) return <OtpAuthForm mfa_token={mfa_token} />;
|
||||
|
||||
return <LoginForm handleSubmit={this.handleSubmit} isLoading={isLoading} />;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import React, { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { logIn, verifyCredentials, switchAccount } from 'soapbox/actions/auth';
|
||||
import { fetchInstance } from 'soapbox/actions/instance';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { getRedirectUrl } from 'soapbox/utils/redirect';
|
||||
import { isStandalone } from 'soapbox/utils/state';
|
||||
|
||||
import LoginForm from './login_form';
|
||||
import OtpAuthForm from './otp_auth_form';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const LoginPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const standalone = useAppSelector((state) => isStandalone(state));
|
||||
|
||||
const token = new URLSearchParams(window.location.search).get('token');
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [mfaAuthNeeded, setMfaAuthNeeded] = useState(!!token);
|
||||
const [mfaToken, setMfaToken] = useState(token || '');
|
||||
const [shouldRedirect, setShouldRedirect] = useState(false);
|
||||
|
||||
const getFormData = (form: HTMLFormElement) => {
|
||||
return Object.fromEntries(
|
||||
Array.from(form).map((i: any) => [i.name, i.value]),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
const { username, password } = getFormData(event.target as HTMLFormElement);
|
||||
dispatch(logIn(intl, username, password)).then(({ access_token }: { access_token: string }) => {
|
||||
return dispatch(verifyCredentials(access_token))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance() as any));
|
||||
}).then((account: { id: string }) => {
|
||||
dispatch(closeModal());
|
||||
setShouldRedirect(true);
|
||||
if (typeof me === 'string') {
|
||||
dispatch(switchAccount(account.id));
|
||||
}
|
||||
}).catch((error: AxiosError) => {
|
||||
const data: any = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaAuthNeeded(true);
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
setIsLoading(true);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
if (standalone) return <Redirect to='/login/external' />;
|
||||
|
||||
if (shouldRedirect) {
|
||||
const redirectUri = getRedirectUrl();
|
||||
return <Redirect to={redirectUri} />;
|
||||
}
|
||||
|
||||
if (mfaAuthNeeded) return <OtpAuthForm mfa_token={mfaToken} />;
|
||||
|
||||
return <LoginForm handleSubmit={handleSubmit} isLoading={isLoading} />;
|
||||
};
|
||||
|
||||
export default LoginPage;
|
|
@ -1,96 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
fetchBackups,
|
||||
createBackup,
|
||||
} from 'soapbox/actions/backups';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
|
||||
import Column from '../ui/components/better_column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.backups', defaultMessage: 'Backups' },
|
||||
create: { id: 'backups.actions.create', defaultMessage: 'Create backup' },
|
||||
emptyMessage: { id: 'backups.empty_message', defaultMessage: 'No backups found. {action}' },
|
||||
emptyMessageAction: { id: 'backups.empty_message.action', defaultMessage: 'Create one now?' },
|
||||
pending: { id: 'backups.pending', defaultMessage: 'Pending' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
backups: state.get('backups').toList().sortBy(backup => backup.get('inserted_at')),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Backups extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
isLoading: true,
|
||||
}
|
||||
|
||||
handleCreateBackup = e => {
|
||||
this.props.dispatch(createBackup());
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBackups()).then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
makeColumnMenu = () => {
|
||||
const { intl } = this.props;
|
||||
|
||||
return [{
|
||||
text: intl.formatMessage(messages.create),
|
||||
action: this.handleCreateBackup,
|
||||
icon: require('@tabler/icons/icons/plus.svg'),
|
||||
}];
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, backups } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
const showLoading = isLoading && backups.count() === 0;
|
||||
|
||||
const emptyMessageAction = (
|
||||
<a href='#' onClick={this.handleCreateBackup}>
|
||||
{intl.formatMessage(messages.emptyMessageAction)}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column icon='cloud-download' label={intl.formatMessage(messages.heading)} menu={this.makeColumnMenu()}>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
scrollKey='backups'
|
||||
emptyMessage={intl.formatMessage(messages.emptyMessage, { action: emptyMessageAction })}
|
||||
>
|
||||
{backups.map(backup => (
|
||||
<div
|
||||
className={classNames('backup', { 'backup--pending': !backup.get('processed') })}
|
||||
key={backup.get('id')}
|
||||
>
|
||||
{backup.get('processed')
|
||||
? <a href={backup.get('url')} target='_blank'>{backup.get('inserted_at')}</a>
|
||||
: <div>{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
fetchBackups,
|
||||
createBackup,
|
||||
} from 'soapbox/actions/backups';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/better_column';
|
||||
|
||||
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.backups', defaultMessage: 'Backups' },
|
||||
create: { id: 'backups.actions.create', defaultMessage: 'Create backup' },
|
||||
emptyMessage: { id: 'backups.empty_message', defaultMessage: 'No backups found. {action}' },
|
||||
emptyMessageAction: { id: 'backups.empty_message.action', defaultMessage: 'Create one now?' },
|
||||
pending: { id: 'backups.pending', defaultMessage: 'Pending' },
|
||||
});
|
||||
|
||||
const Backups = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const backups = useAppSelector<ImmutableList<ImmutableMap<string, any>>>((state) => state.backups.toList().sortBy((backup: ImmutableMap<string, any>) => backup.get('inserted_at')));
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const handleCreateBackup: React.MouseEventHandler<HTMLAnchorElement> = e => {
|
||||
dispatch(createBackup());
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const makeColumnMenu = () => {
|
||||
return [{
|
||||
text: intl.formatMessage(messages.create),
|
||||
action: handleCreateBackup,
|
||||
icon: require('@tabler/icons/icons/plus.svg'),
|
||||
}];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBackups()).then(() => {
|
||||
setIsLoading(true);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
const showLoading = isLoading && backups.count() === 0;
|
||||
|
||||
const emptyMessageAction = (
|
||||
<a href='#' onClick={handleCreateBackup}>
|
||||
{intl.formatMessage(messages.emptyMessageAction)}
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column icon='cloud-download' label={intl.formatMessage(messages.heading)} menu={makeColumnMenu()}>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
scrollKey='backups'
|
||||
emptyMessage={intl.formatMessage(messages.emptyMessage, { action: emptyMessageAction })}
|
||||
>
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
className={classNames('backup', { 'backup--pending': !backup.get('processed') })}
|
||||
key={backup.get('id')}
|
||||
>
|
||||
{backup.get('processed')
|
||||
? <a href={backup.get('url')} target='_blank'>{backup.get('inserted_at')}</a>
|
||||
: <div>{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}</div>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Backups;
|
|
@ -1,96 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { connectCommunityStream } from 'soapbox/actions/streaming';
|
||||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const onlyMedia = getSettings(state).getIn(['community', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = 'community';
|
||||
|
||||
return {
|
||||
timelineId,
|
||||
onlyMedia,
|
||||
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class CommunityTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
|
||||
this.disconnect();
|
||||
dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
return dispatch(expandCommunityTimeline({ onlyMedia }));
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
const { intl, onlyMedia, timelineId } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onRefresh={this.handleRefresh}
|
||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { connectCommunityStream } from 'soapbox/actions/streaming';
|
||||
import { expandCommunityTimeline } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.community', defaultMessage: 'Local timeline' },
|
||||
});
|
||||
|
||||
const CommunityTimeline = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useSettings();
|
||||
const onlyMedia = settings.getIn(['community', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = 'community';
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
return dispatch(expandCommunityTimeline({ onlyMedia } as any));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(expandCommunityTimeline({ onlyMedia } as any));
|
||||
const disconnect = dispatch(connectCommunityStream({ onlyMedia } as any));
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
onRefresh={handleRefresh}
|
||||
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityTimeline;
|
|
@ -50,8 +50,8 @@ const Search = (props: ISearch) => {
|
|||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
const value = useAppSelector((state) => state.search.get('value'));
|
||||
const submitted = useAppSelector((state) => state.search.get('submitted'));
|
||||
const value = useAppSelector((state) => state.search.value);
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
|
||||
const debouncedSubmit = debounce(() => {
|
||||
dispatch(submitSearch());
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import Hashtag from 'soapbox/components/hashtag';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import { Tabs } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import StatusContainer from 'soapbox/containers/status_container';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
||||
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||
|
||||
const messages = defineMessages({
|
||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
|
||||
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
class SearchResults extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
results: ImmutablePropTypes.map.isRequired,
|
||||
submitted: PropTypes.bool,
|
||||
expandSearch: PropTypes.func.isRequired,
|
||||
selectedFilter: PropTypes.string.isRequired,
|
||||
selectFilter: PropTypes.func.isRequired,
|
||||
features: PropTypes.object.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
trendingStatuses: ImmutablePropTypes.list,
|
||||
trends: ImmutablePropTypes.list,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
|
||||
|
||||
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchTrendingStatuses();
|
||||
}
|
||||
|
||||
renderFilterBar() {
|
||||
const { intl, selectedFilter } = this.props;
|
||||
|
||||
const items = [
|
||||
{
|
||||
text: intl.formatMessage(messages.accounts),
|
||||
action: () => this.handleSelectFilter('accounts'),
|
||||
name: 'accounts',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.statuses),
|
||||
action: () => this.handleSelectFilter('statuses'),
|
||||
name: 'statuses',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.hashtags),
|
||||
action: () => this.handleSelectFilter('hashtags'),
|
||||
name: 'hashtags',
|
||||
},
|
||||
];
|
||||
|
||||
return <Tabs items={items} activeItem={selectedFilter} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { value, results, submitted, selectedFilter, suggestions, trendingStatuses, trends } = this.props;
|
||||
|
||||
let searchResults;
|
||||
let hasMore = false;
|
||||
let loaded;
|
||||
let noResultsMessage;
|
||||
let placeholderComponent = PlaceholderStatus;
|
||||
|
||||
if (selectedFilter === 'accounts') {
|
||||
hasMore = results.get('accountsHasMore');
|
||||
loaded = results.get('accountsLoaded');
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
|
||||
if (results.get('accounts') && results.get('accounts').size > 0) {
|
||||
searchResults = results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.get('account')} id={suggestion.get('account')} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.accounts'
|
||||
defaultMessage='There are no people results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'statuses') {
|
||||
hasMore = results.get('statusesHasMore');
|
||||
loaded = results.get('statusesLoaded');
|
||||
|
||||
if (results.get('statuses') && results.get('statuses').size > 0) {
|
||||
searchResults = results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />);
|
||||
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
|
||||
searchResults = trendingStatuses.map(statusId => <StatusContainer key={statusId} id={statusId} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.statuses'
|
||||
defaultMessage='There are no posts results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags') {
|
||||
hasMore = results.get('hashtagsHasMore');
|
||||
loaded = results.get('hashtagsLoaded');
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
|
||||
if (results.get('hashtags') && results.get('hashtags').size > 0) {
|
||||
searchResults = results.get('hashtags').map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = trends.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.hashtags'
|
||||
defaultMessage='There are no hashtags results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.renderFilterBar()}
|
||||
|
||||
{noResultsMessage || (
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && results.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
className={classNames({
|
||||
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
|
||||
})}
|
||||
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
|
||||
>
|
||||
{searchResults}
|
||||
</ScrollableList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { useEffect } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { expandSearch, setFilter } from 'soapbox/actions/search';
|
||||
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
|
||||
import Hashtag from 'soapbox/components/hashtag';
|
||||
import ScrollableList from 'soapbox/components/scrollable_list';
|
||||
import { Tabs } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
import StatusContainer from 'soapbox/containers/status_container';
|
||||
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
|
||||
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
|
||||
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { SearchFilter } from 'soapbox/reducers/search';
|
||||
|
||||
const messages = defineMessages({
|
||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
|
||||
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
|
||||
});
|
||||
|
||||
const SearchResults = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const value = useAppSelector((state) => state.search.submittedValue);
|
||||
const results = useAppSelector((state) => state.search.results);
|
||||
const suggestions = useAppSelector((state) => state.suggestions.items);
|
||||
const trendingStatuses = useAppSelector((state) => state.trending_statuses.items);
|
||||
const trends = useAppSelector((state) => state.trends.items);
|
||||
const submitted = useAppSelector((state) => state.search.submitted);
|
||||
const selectedFilter = useAppSelector((state) => state.search.filter);
|
||||
|
||||
const handleLoadMore = () => dispatch(expandSearch(selectedFilter));
|
||||
|
||||
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
|
||||
|
||||
const renderFilterBar = () => {
|
||||
const items = [
|
||||
{
|
||||
text: intl.formatMessage(messages.accounts),
|
||||
action: () => selectFilter('accounts'),
|
||||
name: 'accounts',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.statuses),
|
||||
action: () => selectFilter('statuses'),
|
||||
name: 'statuses',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.hashtags),
|
||||
action: () => selectFilter('hashtags'),
|
||||
name: 'hashtags',
|
||||
},
|
||||
];
|
||||
|
||||
return <Tabs items={items} activeItem={selectedFilter} />;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchTrendingStatuses());
|
||||
}, []);
|
||||
|
||||
let searchResults;
|
||||
let hasMore = false;
|
||||
let loaded;
|
||||
let noResultsMessage;
|
||||
let placeholderComponent = PlaceholderStatus as React.ComponentType;
|
||||
|
||||
if (selectedFilter === 'accounts') {
|
||||
hasMore = results.accountsHasMore;
|
||||
loaded = results.accountsLoaded;
|
||||
placeholderComponent = PlaceholderAccount;
|
||||
|
||||
if (results.accounts && results.accounts.size > 0) {
|
||||
searchResults = results.accounts.map(accountId => <AccountContainer key={accountId} id={accountId} />);
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = suggestions.map(suggestion => <AccountContainer key={suggestion.account} id={suggestion.account} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.accounts'
|
||||
defaultMessage='There are no people results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'statuses') {
|
||||
hasMore = results.statusesHasMore;
|
||||
loaded = results.statusesLoaded;
|
||||
|
||||
if (results.statuses && results.statuses.size > 0) {
|
||||
searchResults = results.statuses.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer key={statusId} id={statusId} />
|
||||
));
|
||||
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
|
||||
searchResults = trendingStatuses.map((statusId: string) => (
|
||||
// @ts-ignore
|
||||
<StatusContainer key={statusId} id={statusId} />
|
||||
));
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.statuses'
|
||||
defaultMessage='There are no posts results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilter === 'hashtags') {
|
||||
hasMore = results.hashtagsHasMore;
|
||||
loaded = results.hashtagsLoaded;
|
||||
placeholderComponent = PlaceholderHashtag;
|
||||
|
||||
if (results.hashtags && results.hashtags.size > 0) {
|
||||
searchResults = results.hashtags.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
} else if (!submitted && suggestions && !suggestions.isEmpty()) {
|
||||
searchResults = trends.map(hashtag => <Hashtag key={hashtag.name} hashtag={hashtag} />);
|
||||
} else if (loaded) {
|
||||
noResultsMessage = (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage
|
||||
id='empty_column.search.hashtags'
|
||||
defaultMessage='There are no hashtags results for "{term}"'
|
||||
values={{ term: value }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderFilterBar()}
|
||||
|
||||
{noResultsMessage || (
|
||||
<ScrollableList
|
||||
key={selectedFilter}
|
||||
scrollKey={`${selectedFilter}:${value}`}
|
||||
isLoading={submitted && !loaded}
|
||||
showLoading={submitted && !loaded && searchResults?.isEmpty()}
|
||||
hasMore={hasMore}
|
||||
onLoadMore={handleLoadMore}
|
||||
placeholderComponent={placeholderComponent}
|
||||
placeholderCount={20}
|
||||
className={classNames({
|
||||
'divide-gray-200 dark:divide-slate-700 divide-solid divide-y': selectedFilter === 'statuses',
|
||||
})}
|
||||
itemClassName={classNames({ 'pb-4': selectedFilter === 'accounts' })}
|
||||
>
|
||||
{searchResults || []}
|
||||
</ScrollableList>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResults;
|
|
@ -1,33 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { expandSearch, setFilter } from 'soapbox/actions/search';
|
||||
import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions';
|
||||
import { fetchTrendingStatuses } from 'soapbox/actions/trending_statuses';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import SearchResults from '../components/search_results';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
value: state.getIn(['search', 'submittedValue']),
|
||||
results: state.getIn(['search', 'results']),
|
||||
suggestions: state.getIn(['suggestions', 'items']),
|
||||
trendingStatuses: state.getIn(['trending_statuses', 'items']),
|
||||
trends: state.getIn(['trends', 'items']),
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
selectedFilter: state.getIn(['search', 'filter']),
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
fetchSuggestions: () => dispatch(fetchSuggestions()),
|
||||
fetchTrendingStatuses: () => dispatch(fetchTrendingStatuses()),
|
||||
expandSearch: type => dispatch(expandSearch(type)),
|
||||
dismissSuggestion: account => dispatch(dismissSuggestion(account.get('id'))),
|
||||
selectFilter: newActiveFilter => dispatch(setFilter(newActiveFilter)),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
|
|
@ -6,7 +6,6 @@ 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({
|
||||
|
|
|
@ -13,7 +13,6 @@ const emojiMap = require('./emoji_map.json');
|
|||
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
|
||||
|
||||
if (data.compressed) {
|
||||
data = emojiMartUncompress(data);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ const mapStateToProps = state => ({
|
|||
filters: state.get('filters'),
|
||||
});
|
||||
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class Filters extends ImmutablePureComponent {
|
||||
|
|
|
@ -21,7 +21,6 @@ const CSVImporter: React.FC<ICSVImporter> = ({ messages, action }) => {
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [file, setFile] = useState<File | null | undefined>(null);
|
||||
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
const params = new FormData();
|
||||
params.append('list', file!);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -10,6 +9,8 @@ import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soap
|
|||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import resizeImage from 'soapbox/utils/resize_image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
/** Default avatar filenames from various backends */
|
||||
const DEFAULT_AVATARS = [
|
||||
'/avatars/original/missing.png', // Mastodon
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -8,6 +7,8 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import { Button, Card, CardBody, FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const BioStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -11,6 +10,8 @@ import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soap
|
|||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import resizeImage from 'soapbox/utils/resize_image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
'/headers/original/missing.png', // Mastodon
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -8,6 +7,8 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import { Button, Card, CardBody, FormGroup, Input, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
|
@ -1,145 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { changeSetting, getSettings } from 'soapbox/actions/settings';
|
||||
import { connectPublicStream } from 'soapbox/actions/streaming';
|
||||
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||
|
||||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
||||
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const settings = getSettings(state);
|
||||
const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = 'public';
|
||||
|
||||
return {
|
||||
timelineId,
|
||||
onlyMedia,
|
||||
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
|
||||
siteTitle: state.getIn(['instance', 'title']),
|
||||
explanationBoxExpanded: settings.get('explanationBox'),
|
||||
showExplanationBox: settings.get('showExplanationBox'),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class CommunityTimeline extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
onlyMedia: PropTypes.bool,
|
||||
timelineId: PropTypes.string,
|
||||
siteTitle: PropTypes.string,
|
||||
showExplanationBox: PropTypes.bool,
|
||||
explanationBoxExpanded: PropTypes.bool,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.onlyMedia !== this.props.onlyMedia) {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
this.disconnect();
|
||||
|
||||
dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
explanationBoxMenu = () => {
|
||||
const { intl } = this.props;
|
||||
return [{ text: intl.formatMessage(messages.dismiss), action: this.dismissExplanationBox }];
|
||||
}
|
||||
|
||||
dismissExplanationBox = () => {
|
||||
this.props.dispatch(changeSetting(['showExplanationBox'], false));
|
||||
}
|
||||
|
||||
toggleExplanationBox = (setting) => {
|
||||
this.props.dispatch(changeSetting(['explanationBox'], setting));
|
||||
}
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
const { dispatch, onlyMedia } = this.props;
|
||||
return dispatch(expandPublicTimeline({ onlyMedia }));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, onlyMedia, timelineId, siteTitle, showExplanationBox, explanationBoxExpanded } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<PinnedHostsPicker />
|
||||
{showExplanationBox && <div className='explanation-box'>
|
||||
<Accordion
|
||||
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
||||
menu={this.explanationBoxMenu()}
|
||||
expanded={explanationBoxExpanded}
|
||||
onToggle={this.toggleExplanationBox}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='fediverse_tab.explanation_box.explanation'
|
||||
defaultMessage='{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka "servers"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don't like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.'
|
||||
values={{
|
||||
site_title: siteTitle,
|
||||
local: (
|
||||
<Link to='/timeline/local'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.local_tab'
|
||||
defaultMessage='the {site_title} tab'
|
||||
values={{ site_title: siteTitle }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>}
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={this.handleLoadMore}
|
||||
onRefresh={this.handleRefresh}
|
||||
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { changeSetting } from 'soapbox/actions/settings';
|
||||
import { connectPublicStream } from 'soapbox/actions/streaming';
|
||||
import { expandPublicTimeline } from 'soapbox/actions/timelines';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import Accordion from 'soapbox/features/ui/components/accordion';
|
||||
import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import PinnedHostsPicker from '../remote_timeline/components/pinned_hosts_picker';
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
import ColumnSettings from './containers/column_settings_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.public', defaultMessage: 'Fediverse timeline' },
|
||||
dismiss: { id: 'fediverse_tab.explanation_box.dismiss', defaultMessage: 'Don\'t show again' },
|
||||
});
|
||||
|
||||
const CommunityTimeline = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useSettings();
|
||||
const onlyMedia = settings.getIn(['public', 'other', 'onlyMedia']);
|
||||
|
||||
const timelineId = 'public';
|
||||
|
||||
const siteTitle = useAppSelector((state) => state.instance.title);
|
||||
const explanationBoxExpanded = settings.get('explanationBox');
|
||||
const showExplanationBox = settings.get('showExplanationBox');
|
||||
|
||||
const explanationBoxMenu = () => {
|
||||
return [{ text: intl.formatMessage(messages.dismiss), action: dismissExplanationBox }];
|
||||
};
|
||||
|
||||
const dismissExplanationBox = () => {
|
||||
dispatch(changeSetting(['showExplanationBox'], false));
|
||||
};
|
||||
|
||||
const toggleExplanationBox = (setting: boolean) => {
|
||||
dispatch(changeSetting(['explanationBox'], setting));
|
||||
};
|
||||
|
||||
const handleLoadMore = (maxId: string) => {
|
||||
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
return dispatch(expandPublicTimeline({ onlyMedia } as any));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(expandPublicTimeline({ onlyMedia } as any));
|
||||
const disconnect = dispatch(connectPublicStream({ onlyMedia }));
|
||||
|
||||
return () => {
|
||||
disconnect();
|
||||
};
|
||||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent>
|
||||
<SubNavigation message={intl.formatMessage(messages.title)} settings={ColumnSettings} />
|
||||
<PinnedHostsPicker />
|
||||
{showExplanationBox && <div className='mb-4'>
|
||||
<Accordion
|
||||
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
|
||||
menu={explanationBoxMenu()}
|
||||
expanded={explanationBoxExpanded}
|
||||
onToggle={toggleExplanationBox}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='fediverse_tab.explanation_box.explanation'
|
||||
defaultMessage='{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka "servers"). The posts you see here are from 3rd-party servers. You have the freedom to engage with them, or to block any server you don't like. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.'
|
||||
values={{
|
||||
site_title: siteTitle,
|
||||
local: (
|
||||
<Link to='/timeline/local'>
|
||||
<FormattedMessage
|
||||
id='empty_column.home.local_tab'
|
||||
defaultMessage='the {site_title} tab'
|
||||
values={{ site_title: siteTitle }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Accordion>
|
||||
</div>}
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
|
||||
onLoadMore={handleLoadMore}
|
||||
onRefresh={handleRefresh}
|
||||
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
|
||||
divideType='space'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommunityTimeline;
|
|
@ -7,7 +7,7 @@ import { useSettings } from 'soapbox/hooks';
|
|||
|
||||
interface IPinnedHostsPicker {
|
||||
/** The active host among pinned hosts. */
|
||||
host: string,
|
||||
host?: string,
|
||||
}
|
||||
|
||||
const PinnedHostsPicker: React.FC<IPinnedHostsPicker> = ({ host: activeHost }) => {
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
import noop from 'lodash/noop';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import Bundle from '../../ui/components/bundle';
|
||||
import { MediaGallery, Video, Audio } from '../../ui/util/async-components';
|
||||
|
||||
export default class StatusCheckBox extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.record.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onToggle: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { status, checked, onToggle, disabled } = this.props;
|
||||
let media = null;
|
||||
|
||||
if (status.get('reblog')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
|
||||
// Do nothing
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
const video = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
preview={video.get('preview_url')}
|
||||
blurhash={video.get('blurhash')}
|
||||
src={video.get('url')}
|
||||
alt={video.get('description')}
|
||||
aspectRatio={video.getIn(['meta', 'original', 'aspect'])}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const audio = status.getIn(['media_attachments', 0]);
|
||||
|
||||
media = (
|
||||
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
|
||||
{Component => (
|
||||
<Component
|
||||
src={audio.get('url')}
|
||||
alt={audio.get('description')}
|
||||
inline
|
||||
sensitive={status.get('sensitive')}
|
||||
onOpenAudio={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery} >
|
||||
{Component => <Component media={status.get('media_attachments')} sensitive={status.get('sensitive')} height={110} onOpenMedia={noop} />}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status-check-box'>
|
||||
<div className='status-check-box__status'>
|
||||
<StatusContent status={status} />
|
||||
{media}
|
||||
</div>
|
||||
|
||||
<div className='status-check-box-toggle'>
|
||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} icons={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import noop from 'lodash/noop';
|
||||
import React from 'react';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import { toggleStatusReport } from 'soapbox/actions/reports';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Bundle from '../../ui/components/bundle';
|
||||
import { MediaGallery, Video, Audio } from '../../ui/util/async-components';
|
||||
|
||||
interface IStatusCheckBox {
|
||||
id: string,
|
||||
disabled?: boolean,
|
||||
}
|
||||
|
||||
const StatusCheckBox: React.FC<IStatusCheckBox> = ({ id, disabled }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const status = useAppSelector((state) => state.statuses.get(id));
|
||||
const checked = useAppSelector((state) => state.reports.new.status_ids.includes(id));
|
||||
|
||||
const onToggle: React.ChangeEventHandler<HTMLInputElement> = (e) => dispatch(toggleStatusReport(id, e.target.checked));
|
||||
|
||||
if (!status || status.reblog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let media;
|
||||
|
||||
if (status.media_attachments.size > 0) {
|
||||
if (status.media_attachments.some(item => item.type === 'unknown')) {
|
||||
// Do nothing
|
||||
} else if (status.media_attachments.get(0)?.type === 'video') {
|
||||
const video = status.media_attachments.get(0);
|
||||
|
||||
if (video) {
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} >
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
preview={video.preview_url}
|
||||
blurhash={video.blurhash}
|
||||
src={video.url}
|
||||
alt={video.description}
|
||||
aspectRatio={video.meta.getIn(['original', 'aspect'])}
|
||||
width={239}
|
||||
height={110}
|
||||
inline
|
||||
sensitive={status.sensitive}
|
||||
onOpenVideo={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
} else if (status.media_attachments.get(0)?.type === 'audio') {
|
||||
const audio = status.media_attachments.get(0);
|
||||
|
||||
if (audio) {
|
||||
media = (
|
||||
<Bundle fetchComponent={Audio} >
|
||||
{(Component: any) => (
|
||||
<Component
|
||||
src={audio.url}
|
||||
alt={audio.description}
|
||||
inline
|
||||
sensitive={status.sensitive}
|
||||
onOpenAudio={noop}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} >
|
||||
{(Component: any) => <Component media={status.media_attachments} sensitive={status.sensitive} height={110} onOpenMedia={noop} />}
|
||||
</Bundle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='status-check-box'>
|
||||
<div className='status-check-box__status'>
|
||||
<StatusContent status={status} />
|
||||
{media}
|
||||
</div>
|
||||
|
||||
<div className='status-check-box-toggle'>
|
||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} icons={false} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusCheckBox;
|
|
@ -1,20 +0,0 @@
|
|||
import { Set as ImmutableSet } from 'immutable';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { toggleStatusReport } from '../../../actions/reports';
|
||||
import StatusCheckBox from '../components/status_check_box';
|
||||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
status: state.getIn(['statuses', id]),
|
||||
checked: state.getIn(['reports', 'new', 'status_ids'], ImmutableSet()).includes(id),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { id }) => ({
|
||||
|
||||
onToggle(e) {
|
||||
dispatch(toggleStatusReport(id, e.target.checked));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusCheckBox);
|
|
@ -1,60 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['search', 'value']),
|
||||
submitted: state.getIn(['search', 'submitted']),
|
||||
});
|
||||
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
submitted: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
submittedValue: '',
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.submitted) {
|
||||
const submittedValue = this.props.value;
|
||||
this.setState({ submittedValue });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { submittedValue } = this.state;
|
||||
|
||||
if (!submittedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='search-header'>
|
||||
<div className='search-header__text-container'>
|
||||
<h1 className='search-header__title-text'>
|
||||
{submittedValue}
|
||||
</h1>
|
||||
</div>
|
||||
<div className='search-header__type-filters'>
|
||||
<div className='account__section-headline'>
|
||||
<div className='search-header__type-filters-tabs'>
|
||||
<NavLink to='/search' activeClassName='active'>
|
||||
<FormattedMessage id='search_results.top' defaultMessage='Top' />
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(Header);
|
|
@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import Search from 'soapbox/features/compose/components/search';
|
||||
import SearchResultsContainer from 'soapbox/features/compose/containers/search_results_container';
|
||||
import SearchResults from 'soapbox/features/compose/components/search_results';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.search', defaultMessage: 'Search' },
|
||||
|
@ -16,7 +16,7 @@ const SearchPage = () => {
|
|||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<div className='space-y-4'>
|
||||
<Search autoFocus autoSubmit />
|
||||
<SearchResultsContainer />
|
||||
<SearchResults />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -14,7 +14,6 @@ const messages = defineMessages({
|
|||
passwordPlaceholder: { id: 'mfa.mfa_setup.password_placeholder', defaultMessage: 'Password' },
|
||||
});
|
||||
|
||||
|
||||
const DisableOtpForm: React.FC = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [password, setPassword] = useState('');
|
||||
|
@ -39,7 +38,6 @@ const DisableOtpForm: React.FC = () => {
|
|||
setPassword(event.target.value);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Stack>
|
||||
|
|
|
@ -29,7 +29,6 @@ const MfaForm: React.FC = () => {
|
|||
dispatch(fetchMfa());
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSetupProceedClick = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setDisplayOtpForm(true);
|
||||
|
|
|
@ -525,7 +525,6 @@ class ActionBar extends React.PureComponent<IActionBar, IActionBarState> {
|
|||
|
||||
const canShare = ('share' in navigator) && status.visibility === 'public';
|
||||
|
||||
|
||||
let reblogIcon = require('@tabler/icons/icons/repeat.svg');
|
||||
|
||||
if (status.visibility === 'direct') {
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import { normalizeTag } from '../../../../normalizers';
|
||||
import TrendsPanel from '../trends-panel';
|
||||
|
||||
describe('<TrendsPanel />', () => {
|
||||
it('renders trending hashtags', () => {
|
||||
const store = {
|
||||
trends: ImmutableMap({
|
||||
items: fromJS([{
|
||||
trends: ImmutableRecord({
|
||||
items: ImmutableList([
|
||||
normalizeTag({
|
||||
name: 'hashtag 1',
|
||||
history: [{
|
||||
day: '1652745600',
|
||||
uses: '294',
|
||||
accounts: '180',
|
||||
}],
|
||||
}]),
|
||||
}),
|
||||
]),
|
||||
isLoading: false,
|
||||
})(),
|
||||
};
|
||||
|
||||
render(<TrendsPanel limit={1} />, null, store);
|
||||
|
@ -27,18 +31,19 @@ describe('<TrendsPanel />', () => {
|
|||
|
||||
it('renders multiple trends', () => {
|
||||
const store = {
|
||||
trends: ImmutableMap({
|
||||
items: fromJS([
|
||||
{
|
||||
trends: ImmutableRecord({
|
||||
items: ImmutableList([
|
||||
normalizeTag({
|
||||
name: 'hashtag 1',
|
||||
history: [{ accounts: [] }],
|
||||
},
|
||||
{
|
||||
name: 'hashtag 2',
|
||||
history: [{ accounts: [] }],
|
||||
},
|
||||
]),
|
||||
history: ImmutableList([{ accounts: [] }]),
|
||||
}),
|
||||
normalizeTag({
|
||||
name: 'hashtag 2',
|
||||
history: ImmutableList([{ accounts: [] }]),
|
||||
}),
|
||||
]),
|
||||
isLoading: false,
|
||||
})(),
|
||||
};
|
||||
|
||||
render(<TrendsPanel limit={3} />, null, store);
|
||||
|
@ -47,18 +52,19 @@ describe('<TrendsPanel />', () => {
|
|||
|
||||
it('respects the limit prop', () => {
|
||||
const store = {
|
||||
trends: ImmutableMap({
|
||||
items: fromJS([
|
||||
{
|
||||
trends: ImmutableRecord({
|
||||
items: ImmutableList([
|
||||
normalizeTag({
|
||||
name: 'hashtag 1',
|
||||
history: [{ accounts: [] }],
|
||||
},
|
||||
{
|
||||
}),
|
||||
normalizeTag({
|
||||
name: 'hashtag 2',
|
||||
history: [{ accounts: [] }],
|
||||
},
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
isLoading: false,
|
||||
})(),
|
||||
};
|
||||
|
||||
render(<TrendsPanel limit={1} />, null, store);
|
||||
|
@ -67,9 +73,10 @@ describe('<TrendsPanel />', () => {
|
|||
|
||||
it('renders empty', () => {
|
||||
const store = {
|
||||
trends: ImmutableMap({
|
||||
items: fromJS([]),
|
||||
}),
|
||||
trends: ImmutableRecord({
|
||||
items: ImmutableList([]),
|
||||
isLoading: false,
|
||||
})(),
|
||||
};
|
||||
|
||||
render(<TrendsPanel limit={1} />, null, store);
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { Modal, Text } from 'soapbox/components/ui';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
|
||||
save: { id: 'account_note.save', defaultMessage: 'Save' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isSubmitting: state.getIn(['account_notes', 'edit', 'isSubmitting']),
|
||||
account: getAccount(state, state.getIn(['account_notes', 'edit', 'account_id'])),
|
||||
comment: state.getIn(['account_notes', 'edit', 'comment']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onConfirm() {
|
||||
dispatch(submitAccountNote());
|
||||
},
|
||||
|
||||
onClose() {
|
||||
dispatch(closeModal());
|
||||
},
|
||||
|
||||
onCommentChange(comment) {
|
||||
dispatch(changeAccountNoteComment(comment));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class AccountNoteModal extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isSubmitting: PropTypes.bool,
|
||||
account: PropTypes.object.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCommentChange: PropTypes.func.isRequired,
|
||||
comment: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleCommentChange = e => {
|
||||
this.props.onCommentChange(e.target.value);
|
||||
}
|
||||
|
||||
handleSubmit = () => {
|
||||
this.props.onConfirm();
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
this.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, isSubmitting, comment, onClose, intl } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='account_note.target' defaultMessage='Note for @{target}' values={{ target: account.get('acct') }} />}
|
||||
onClose={onClose}
|
||||
confirmationAction={this.handleSubmit}
|
||||
confirmationText={intl.formatMessage(messages.save)}
|
||||
confirmationDisabled={isSubmitting}
|
||||
>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='account_note.hint' defaultMessage='You can keep notes about this user for yourself (this will not be shared with them):' />
|
||||
</Text>
|
||||
|
||||
<textarea
|
||||
className='setting-text light'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={comment}
|
||||
onChange={this.handleCommentChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { changeAccountNoteComment, submitAccountNote } from 'soapbox/actions/account-notes';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { Modal, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'account_note.placeholder', defaultMessage: 'No comment provided' },
|
||||
save: { id: 'account_note.save', defaultMessage: 'Save' },
|
||||
});
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
||||
const AccountNoteModal = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isSubmitting = useAppSelector((state) => state.account_notes.edit.isSubmitting);
|
||||
const account = useAppSelector((state) => getAccount(state, state.account_notes.edit.account!));
|
||||
const comment = useAppSelector((state) => state.account_notes.edit.comment);
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(closeModal('ACCOUNT_NOTE'));
|
||||
};
|
||||
|
||||
const handleCommentChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
|
||||
dispatch(changeAccountNoteComment(e.target.value));
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
dispatch(submitAccountNote());
|
||||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = e => {
|
||||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='account_note.target' defaultMessage='Note for @{target}' values={{ target: account!.acct }} />}
|
||||
onClose={onClose}
|
||||
confirmationAction={handleSubmit}
|
||||
confirmationText={intl.formatMessage(messages.save)}
|
||||
confirmationDisabled={isSubmitting}
|
||||
>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='account_note.hint' defaultMessage='You can keep notes about this user for yourself (this will not be shared with them):' />
|
||||
</Text>
|
||||
|
||||
<textarea
|
||||
className='setting-text light'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={comment}
|
||||
onChange={handleCommentChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountNoteModal;
|
|
@ -1,18 +1,27 @@
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StatusContent from 'soapbox/components/status_content';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account_container';
|
||||
|
||||
import Icon from '../../../components/icon';
|
||||
import StatusContent from '../../../components/status_content';
|
||||
import { Stack } from '../../../components/ui';
|
||||
import AccountContainer from '../../../containers/account_container';
|
||||
import Motion from '../util/optional_motion';
|
||||
|
||||
const ActionsModal = ({ status, actions, onClick, onClose }) => {
|
||||
const renderAction = (action, i) => {
|
||||
import type { Menu, MenuItem } from 'soapbox/components/dropdown_menu';
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface IActionsModal {
|
||||
status: StatusEntity,
|
||||
actions: Menu,
|
||||
onClick: () => void,
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClose }) => {
|
||||
const renderAction = (action: MenuItem | null, i: number) => {
|
||||
if (action === null) {
|
||||
return <li key={`sep-${i}`} className='dropdown-menu__separator' />;
|
||||
}
|
||||
|
@ -48,9 +57,10 @@ const ActionsModal = ({ status, actions, onClick, onClose }) => {
|
|||
{status && (
|
||||
<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')}
|
||||
key={status.account as string}
|
||||
id={status.account as string}
|
||||
showProfileHoverCard={false}
|
||||
timestamp={status.get('created_at')}
|
||||
timestamp={status.created_at}
|
||||
/>
|
||||
<StatusContent status={status} />
|
||||
</Stack>
|
||||
|
@ -73,11 +83,4 @@ const ActionsModal = ({ status, actions, onClick, onClose }) => {
|
|||
);
|
||||
};
|
||||
|
||||
ActionsModal.propTypes = {
|
||||
status: ImmutablePropTypes.record,
|
||||
actions: PropTypes.array,
|
||||
onClick: PropTypes.func,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActionsModal;
|
|
@ -1,37 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import Pullable from 'soapbox/components/pullable';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
|
||||
import ColumnHeader from './column_header';
|
||||
|
||||
export default class UIColumn extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
heading: PropTypes.string,
|
||||
icon: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
active: PropTypes.bool,
|
||||
showBackBtn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
showBackBtn: true,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { heading, icon, children, active, showBackBtn, ...rest } = this.props;
|
||||
const columnHeaderId = heading && heading.replace(/ /g, '-');
|
||||
|
||||
return (
|
||||
<Column aria-labelledby={columnHeaderId} {...rest}>
|
||||
{heading && <ColumnHeader icon={icon} active={active} type={heading} columnHeaderId={columnHeaderId} showBackBtn={showBackBtn} />}
|
||||
<Pullable>
|
||||
{children}
|
||||
</Pullable>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
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;
|
|
@ -1,32 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Column from './column';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column_forbidden.title', defaultMessage: 'Forbidden' },
|
||||
body: { id: 'column_forbidden.body', defaultMessage: 'You do not have permission to access this page.' },
|
||||
});
|
||||
|
||||
class ColumnForbidden extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl: { formatMessage } } = this.props;
|
||||
|
||||
return (
|
||||
<Column label={formatMessage(messages.title)}>
|
||||
<div className='error-column'>
|
||||
{formatMessage(messages.body)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ColumnForbidden);
|
|
@ -0,0 +1,23 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Column from './column';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column_forbidden.title', defaultMessage: 'Forbidden' },
|
||||
body: { id: 'column_forbidden.body', defaultMessage: 'You do not have permission to access this page.' },
|
||||
});
|
||||
|
||||
const ColumnForbidden = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<div className='error-column'>
|
||||
{intl.formatMessage(messages.body)}
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default ColumnForbidden;
|
|
@ -1,40 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
// import classNames from 'classnames';
|
||||
// import Icon from 'soapbox/components/icon';
|
||||
import SubNavigation from 'soapbox/components/sub_navigation';
|
||||
|
||||
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 { type } = this.props;
|
||||
return <SubNavigation message={type} />;
|
||||
}
|
||||
|
||||
// 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>
|
||||
// );
|
||||
// }
|
||||
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
|
||||
// import classNames from 'classnames';
|
||||
// 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>
|
||||
// );
|
||||
// }
|
||||
|
||||
// }
|
|
@ -39,7 +39,6 @@ import BundleContainer from '../containers/bundle_container';
|
|||
import BundleModalError from './bundle_modal_error';
|
||||
import ModalLoading from './modal_loading';
|
||||
|
||||
|
||||
const MODAL_COMPONENTS = {
|
||||
'MEDIA': MediaModal,
|
||||
'VIDEO': VideoModal,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
import { Map as ImmutableMap, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||
import React from 'react';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
|
@ -24,13 +24,13 @@ describe('<ReportModal />', () => {
|
|||
avatar: 'test.jpg',
|
||||
}),
|
||||
}),
|
||||
reports: ImmutableMap({
|
||||
new: {
|
||||
reports: ImmutableRecord({
|
||||
new: ImmutableRecord({
|
||||
account_id: '1',
|
||||
status_ids: ImmutableSet(['1']),
|
||||
rule_ids: ImmutableSet(),
|
||||
},
|
||||
}),
|
||||
})(),
|
||||
})(),
|
||||
statuses: ImmutableMap({
|
||||
'1': normalizeStatus(status),
|
||||
}),
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import { Set as ImmutableSet } from 'immutable';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -16,6 +14,8 @@ import ConfirmationStep from './steps/confirmation-step';
|
|||
import OtherActionsStep from './steps/other-actions-step';
|
||||
import ReasonStep from './steps/reason-step';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
blankslate: { id: 'report.reason.blankslate', defaultMessage: 'You have removed all statuses from being selected.' },
|
||||
done: { id: 'report.done', defaultMessage: 'Done' },
|
||||
|
@ -77,14 +77,14 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
|||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const accountId = useAppSelector((state) => state.reports.getIn(['new', 'account_id']) as string);
|
||||
const account = useAccount(accountId);
|
||||
const accountId = useAppSelector((state) => state.reports.new.account_id);
|
||||
const account = useAccount(accountId as string);
|
||||
|
||||
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
|
||||
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
|
||||
const isBlocked = useAppSelector((state) => state.reports.new.block);
|
||||
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
|
||||
const rules = useAppSelector((state) => state.rules.items);
|
||||
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
|
||||
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
|
||||
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
||||
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
|
||||
|
||||
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
|
||||
const shouldRequireRule = rules.length > 0;
|
||||
|
|
|
@ -7,7 +7,7 @@ import Toggle from 'react-toggle';
|
|||
import { changeReportBlock, changeReportForward } from 'soapbox/actions/reports';
|
||||
import { fetchRules } from 'soapbox/actions/rules';
|
||||
import { Button, FormGroup, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import StatusCheckBox from 'soapbox/features/report/containers/status_check_box_container';
|
||||
import StatusCheckBox from 'soapbox/features/report/components/status_check_box';
|
||||
import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||
import { isRemote, getDomain } from 'soapbox/utils/accounts';
|
||||
|
||||
|
@ -30,11 +30,11 @@ const OtherActionsStep = ({ account }: IOtherActionsStep) => {
|
|||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
|
||||
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${account.id}:with_replies`, 'items'])).union(state.reports.getIn(['new', 'status_ids']) as Iterable<unknown>) as OrderedSet<string>);
|
||||
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
|
||||
const isForward = useAppSelector((state) => state.reports.getIn(['new', 'forward']) as boolean);
|
||||
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${account.id}:with_replies`, 'items'])).union(state.reports.new.status_ids) as OrderedSet<string>);
|
||||
const isBlocked = useAppSelector((state) => state.reports.new.block);
|
||||
const isForward = useAppSelector((state) => state.reports.new.forward);
|
||||
const canForward = isRemote(account as any) && features.federating;
|
||||
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
|
||||
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
|
||||
|
||||
const [showAdditionalStatuses, setShowAdditionalStatuses] = useState<boolean>(false);
|
||||
|
||||
|
@ -61,7 +61,7 @@ const OtherActionsStep = ({ account }: IOtherActionsStep) => {
|
|||
<FormGroup labelText={intl.formatMessage(messages.addAdditionalStatuses)}>
|
||||
{showAdditionalStatuses ? (
|
||||
<Stack space={2}>
|
||||
<div className='bg-gray-100 rounded-lg p-4'>
|
||||
<div className='bg-gray-100 dark:bg-slate-600 rounded-lg p-4'>
|
||||
{statusIds.map((statusId) => <StatusCheckBox id={statusId} key={statusId} />)}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import { fetchRules } from 'soapbox/actions/rules';
|
|||
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { Set as ImmutableSet } from 'immutable';
|
||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -31,12 +30,12 @@ const ReasonStep = (_props: IReasonStep) => {
|
|||
const [isNearBottom, setNearBottom] = useState<boolean>(false);
|
||||
const [isNearTop, setNearTop] = useState<boolean>(true);
|
||||
|
||||
const comment = useAppSelector((state) => state.reports.getIn(['new', 'comment']) as string);
|
||||
const comment = useAppSelector((state) => state.reports.new.comment);
|
||||
const rules = useAppSelector((state) => state.rules.items);
|
||||
const ruleIds = useAppSelector((state) => state.reports.getIn(['new', 'rule_ids']) as ImmutableSet<string>);
|
||||
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
||||
const shouldRequireRule = rules.length > 0;
|
||||
|
||||
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
|
||||
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
|
||||
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
|
||||
|
||||
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
|
|||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']),
|
||||
isSubmitting: state.reports.new.isSubmitting,
|
||||
account: state.getIn(['mutes', 'new', 'account']),
|
||||
notifications: state.getIn(['mutes', 'new', 'notifications']),
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
@ -15,6 +14,8 @@ import { useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
|||
|
||||
import ProfileDropdown from './profile-dropdown';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
login: { id: 'navbar.login.action', defaultMessage: 'Log in' },
|
||||
username: { id: 'navbar.login.username.placeholder', defaultMessage: 'Email or username' },
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -15,10 +14,10 @@ interface ITrendsPanel {
|
|||
const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const trends: any = useAppSelector((state) => state.trends.get('items'));
|
||||
const trends = useAppSelector((state) => state.trends.items);
|
||||
|
||||
const sortedTrends = React.useMemo(() => {
|
||||
return trends.sort((a: ImmutableMap<string, any>, b: ImmutableMap<string, any>) => {
|
||||
return trends.sort((a, b) => {
|
||||
const num_a = Number(a.getIn(['history', 0, 'accounts']));
|
||||
const num_b = Number(b.getIn(['history', 0, 'accounts']));
|
||||
return num_b - num_a;
|
||||
|
@ -35,8 +34,8 @@ const TrendsPanel = ({ limit }: ITrendsPanel) => {
|
|||
|
||||
return (
|
||||
<Widget title={<FormattedMessage id='trends.title' defaultMessage='Trends' />}>
|
||||
{sortedTrends.map((hashtag: ImmutableMap<string, any>) => (
|
||||
<Hashtag key={hashtag.get('name')} hashtag={hashtag} />
|
||||
{sortedTrends.map((hashtag) => (
|
||||
<Hashtag key={hashtag.name} hashtag={hashtag} />
|
||||
))}
|
||||
</Widget>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,6 @@ import { connect } from 'react-redux';
|
|||
import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles';
|
||||
import Bundle from '../components/bundle';
|
||||
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onFetch() {
|
||||
dispatch(fetchBundleRequest());
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import * as React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -8,6 +7,8 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import { confirmEmailVerification } from 'soapbox/actions/verification';
|
||||
import { Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const Statuses = {
|
||||
IDLE: 'IDLE',
|
||||
SUCCESS: 'SUCCESS',
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { AxiosError } from 'axios';
|
||||
import * as React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -15,6 +14,8 @@ import { getRedirectUrl } from 'soapbox/utils/redirect';
|
|||
|
||||
import PasswordIndicator from './components/password-indicator';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
success: {
|
||||
id: 'registrations.success',
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { verifyAge } from 'soapbox/actions/verification';
|
||||
import { Button, Datepicker, Form, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
fail: {
|
||||
|
@ -14,7 +13,7 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
function meetsAgeMinimum(birthday, ageMinimum) {
|
||||
function meetsAgeMinimum(birthday: Date, ageMinimum: number) {
|
||||
const month = birthday.getUTCMonth();
|
||||
const day = birthday.getUTCDate();
|
||||
const year = birthday.getUTCFullYear();
|
||||
|
@ -24,11 +23,11 @@ function meetsAgeMinimum(birthday, ageMinimum) {
|
|||
|
||||
const AgeVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = useSelector((state) => state.verification.get('isLoading'));
|
||||
const ageMinimum = useSelector((state) => state.verification.get('ageMinimum'));
|
||||
const siteTitle = useSelector((state) => state.instance.get('title'));
|
||||
const isLoading = useAppSelector((state) => state.verification.get('isLoading')) as boolean;
|
||||
const ageMinimum = useAppSelector((state) => state.verification.get('ageMinimum')) as any;
|
||||
const siteTitle = useAppSelector((state) => state.instance.title);
|
||||
|
||||
const [date, setDate] = React.useState('');
|
||||
const isValid = typeof date === 'object';
|
||||
|
@ -44,9 +43,7 @@ const AgeVerification = () => {
|
|||
dispatch(verifyAge(birthday));
|
||||
} else {
|
||||
dispatch(
|
||||
snackbar.error(intl.formatMessage(messages.fail, {
|
||||
ageMinimum,
|
||||
})),
|
||||
snackbar.error(intl.formatMessage(messages.fail, { ageMinimum })),
|
||||
);
|
||||
}
|
||||
}, [date, ageMinimum]);
|
||||
|
@ -78,8 +75,4 @@ const AgeVerification = () => {
|
|||
);
|
||||
};
|
||||
|
||||
AgeVerification.propTypes = {
|
||||
verifyAge: PropTypes.func,
|
||||
};
|
||||
|
||||
export default AgeVerification;
|
|
@ -1,12 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import * as React from 'react';
|
||||
import { AxiosError } from 'axios';
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { checkEmailVerification, postEmailVerification, requestEmailVerification } from 'soapbox/actions/verification';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const Statuses = {
|
||||
IDLE: 'IDLE',
|
||||
|
@ -16,8 +16,12 @@ const Statuses = {
|
|||
|
||||
const EMAIL_REGEX = /^[^@\s]+@[^@\s]+$/;
|
||||
|
||||
const EmailSent = ({ handleSubmit }) => {
|
||||
const dispatch = useDispatch();
|
||||
interface IEmailSent {
|
||||
handleSubmit: React.FormEventHandler,
|
||||
}
|
||||
|
||||
const EmailSent: React.FC<IEmailSent> = ({ handleSubmit }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const checkEmailConfirmation = () => {
|
||||
dispatch(checkEmailVerification())
|
||||
|
@ -47,19 +51,19 @@ const EmailSent = ({ handleSubmit }) => {
|
|||
|
||||
const EmailVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = useSelector((state) => state.verification.get('isLoading'));
|
||||
const isLoading = useAppSelector((state) => state.verification.get('isLoading')) as boolean;
|
||||
|
||||
const [email, setEmail] = React.useState('');
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
const [errors, setErrors] = React.useState([]);
|
||||
const [errors, setErrors] = React.useState<Array<string>>([]);
|
||||
|
||||
const isValid = email.length > 0 && EMAIL_REGEX.test(email);
|
||||
|
||||
const onChange = React.useCallback((event) => setEmail(event.target.value), []);
|
||||
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
setErrors([]);
|
||||
|
||||
|
@ -80,8 +84,8 @@ const EmailVerification = () => {
|
|||
),
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
const isEmailTaken = error.response?.data?.error === 'email_taken';
|
||||
.catch((error: AxiosError) => {
|
||||
const isEmailTaken = (error.response?.data as any)?.error === 'email_taken';
|
||||
|
||||
const message = isEmailTaken ? (
|
||||
intl.formatMessage({ id: 'email_verification.exists', defaultMessage: 'This email has already been taken.' })
|
||||
|
@ -130,8 +134,4 @@ const EmailVerification = () => {
|
|||
);
|
||||
};
|
||||
|
||||
EmailSent.propTypes = {
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EmailVerification;
|
|
@ -1,11 +1,11 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import OtpInput from 'react-otp-input';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { confirmPhoneVerification, requestPhoneVerification } from 'soapbox/actions/verification';
|
||||
import { Button, Form, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { formatPhoneNumber } from 'soapbox/utils/phone';
|
||||
|
||||
const Statuses = {
|
||||
|
@ -18,9 +18,9 @@ const validPhoneNumberRegex = /^\+1\s\(\d{3}\)\s\d{3}-\d{4}/;
|
|||
|
||||
const SmsVerification = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = useSelector((state) => state.verification.get('isLoading'));
|
||||
const isLoading = useAppSelector((state) => state.verification.get('isLoading')) as boolean;
|
||||
|
||||
const [phone, setPhone] = React.useState('');
|
||||
const [status, setStatus] = React.useState(Statuses.IDLE);
|
||||
|
@ -165,5 +165,4 @@ const SmsVerification = () => {
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
export { SmsVerification as default, validPhoneNumberRegex };
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* History normalizer:
|
||||
* Converts API daily usage history of a hashtag into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/history/}
|
||||
*/
|
||||
import {
|
||||
Map as ImmutableMap,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/history/
|
||||
export const HistoryRecord = ImmutableRecord({
|
||||
accounts: '',
|
||||
day: '',
|
||||
uses: '',
|
||||
});
|
||||
export const normalizeHistory = (history: Record<string, any>) => {
|
||||
return HistoryRecord(
|
||||
ImmutableMap(fromJS(history)),
|
||||
);
|
||||
};
|
|
@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card';
|
|||
export { ChatRecord, normalizeChat } from './chat';
|
||||
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
|
||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||
export { HistoryRecord, normalizeHistory } from './history';
|
||||
export { InstanceRecord, normalizeInstance } from './instance';
|
||||
export { ListRecord, normalizeList } from './list';
|
||||
export { MentionRecord, normalizeMention } from './mention';
|
||||
|
@ -14,5 +15,6 @@ export { PollRecord, PollOptionRecord, normalizePoll } from './poll';
|
|||
export { RelationshipRecord, normalizeRelationship } from './relationship';
|
||||
export { StatusRecord, normalizeStatus } from './status';
|
||||
export { StatusEditRecord, normalizeStatusEdit } from './status_edit';
|
||||
export { TagRecord, normalizeTag } from './tag';
|
||||
|
||||
export { SoapboxConfigRecord, normalizeSoapboxConfig } from './soapbox/soapbox_config';
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* Tag normalizer:
|
||||
* Converts API tags into our internal format.
|
||||
* @see {@link https://docs.joinmastodon.org/entities/tag/}
|
||||
*/
|
||||
import {
|
||||
List as ImmutableList,
|
||||
Map as ImmutableMap,
|
||||
Record as ImmutableRecord,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
|
||||
import { normalizeHistory } from './history';
|
||||
|
||||
import type { History } from 'soapbox/types/entities';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/tag/
|
||||
export const TagRecord = ImmutableRecord({
|
||||
name: '',
|
||||
url: '',
|
||||
history: null as ImmutableList<History> | null,
|
||||
});
|
||||
|
||||
const normalizeHistoryList = (tag: ImmutableMap<string, any>) => {
|
||||
if (tag.get('history')){
|
||||
return tag.update('history', ImmutableList(), attachments => {
|
||||
return attachments.map(normalizeHistory);
|
||||
});
|
||||
} else {
|
||||
return tag.set('history', null);
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeTag = (tag: Record<string, any>) => {
|
||||
return TagRecord(
|
||||
ImmutableMap(fromJS(tag)).withMutations(tag => {
|
||||
normalizeHistoryList(tag);
|
||||
}),
|
||||
);
|
||||
};
|
|
@ -7,7 +7,6 @@ import reducer from '../accounts_counters';
|
|||
// import accounts_counter_unfollow from 'soapbox/__fixtures__/accounts_counter_unfollow.json';
|
||||
// import accounts_counter_follow from 'soapbox/__fixtures__/accounts_counter_follow.json';
|
||||
|
||||
|
||||
describe('accounts_counters reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap());
|
||||
|
|
|
@ -16,10 +16,10 @@ describe('modal reducer', () => {
|
|||
modalType: 'type1',
|
||||
modalProps: { props1: '1' },
|
||||
};
|
||||
expect(reducer(state, action)).toMatchObject(ImmutableList([{
|
||||
expect(reducer(state, action).toJS()).toMatchObject([{
|
||||
modalType: 'type1',
|
||||
modalProps: { props1: '1' },
|
||||
}]));
|
||||
}]);
|
||||
});
|
||||
|
||||
it('should handle MODAL_CLOSE', () => {
|
||||
|
|
|
@ -1,19 +1,17 @@
|
|||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
import reducer from '../reports';
|
||||
|
||||
describe('reports reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
||||
new: ImmutableMap({
|
||||
expect(reducer(undefined, {}).toJS()).toEqual({
|
||||
new: {
|
||||
isSubmitting: false,
|
||||
account_id: null,
|
||||
status_ids: ImmutableSet(),
|
||||
status_ids: [],
|
||||
comment: '',
|
||||
forward: false,
|
||||
block: false,
|
||||
rule_ids: ImmutableSet(),
|
||||
}),
|
||||
}));
|
||||
rule_ids: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
SEARCH_CHANGE,
|
||||
|
@ -10,14 +10,24 @@ import reducer from '../search';
|
|||
|
||||
describe('search reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
||||
expect(reducer(undefined, {}).toJS()).toEqual({
|
||||
value: '',
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
hidden: false,
|
||||
results: ImmutableMap(),
|
||||
results: {
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
accountsHasMore: false,
|
||||
statusesHasMore: false,
|
||||
hashtagsHasMore: false,
|
||||
accountsLoaded: false,
|
||||
statusesLoaded: false,
|
||||
hashtagsLoaded: false,
|
||||
},
|
||||
filter: 'accounts',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('SEARCH_CHANGE', () => {
|
||||
|
@ -30,42 +40,54 @@ describe('search reducer', () => {
|
|||
|
||||
describe('SEARCH_CLEAR', () => {
|
||||
it('resets the state', () => {
|
||||
const state = ImmutableMap({
|
||||
const state = ImmutableRecord({
|
||||
value: 'hello world',
|
||||
submitted: true,
|
||||
submittedValue: 'hello world',
|
||||
hidden: false,
|
||||
results: ImmutableMap(),
|
||||
results: ImmutableRecord({})(),
|
||||
filter: 'statuses',
|
||||
});
|
||||
})();
|
||||
|
||||
const action = { type: SEARCH_CLEAR };
|
||||
|
||||
const expected = ImmutableMap({
|
||||
const expected = {
|
||||
value: '',
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
hidden: false,
|
||||
results: ImmutableMap(),
|
||||
results: {
|
||||
accounts: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
accountsHasMore: false,
|
||||
statusesHasMore: false,
|
||||
hashtagsHasMore: false,
|
||||
accountsLoaded: false,
|
||||
statusesLoaded: false,
|
||||
hashtagsLoaded: false,
|
||||
},
|
||||
filter: 'accounts',
|
||||
});
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
expect(reducer(state, action).toJS()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe(SEARCH_EXPAND_SUCCESS, () => {
|
||||
it('imports hashtags as maps', () => {
|
||||
const state = ImmutableMap({
|
||||
const state = ImmutableRecord({
|
||||
value: 'artist',
|
||||
submitted: true,
|
||||
submittedValue: 'artist',
|
||||
hidden: false,
|
||||
results: ImmutableMap({
|
||||
results: ImmutableRecord({
|
||||
hashtags: ImmutableList(),
|
||||
}),
|
||||
hashtagsHasMore: false,
|
||||
hashtagsLoaded: true,
|
||||
})(),
|
||||
filter: 'hashtags',
|
||||
});
|
||||
})();
|
||||
|
||||
const action = {
|
||||
type: SEARCH_EXPAND_SUCCESS,
|
||||
|
@ -82,24 +104,26 @@ describe('search reducer', () => {
|
|||
searchType: 'hashtags',
|
||||
};
|
||||
|
||||
const expected = ImmutableMap({
|
||||
const expected = {
|
||||
value: 'artist',
|
||||
submitted: true,
|
||||
submittedValue: 'artist',
|
||||
hidden: false,
|
||||
results: ImmutableMap({
|
||||
hashtags: fromJS([{
|
||||
results: {
|
||||
hashtags: [
|
||||
{
|
||||
name: 'artist',
|
||||
url: 'https://gleasonator.com/tags/artist',
|
||||
history: [],
|
||||
}]),
|
||||
},
|
||||
],
|
||||
hashtagsHasMore: false,
|
||||
hashtagsLoaded: true,
|
||||
}),
|
||||
},
|
||||
filter: 'hashtags',
|
||||
});
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toEqual(expected);
|
||||
expect(reducer(state, action).toJS()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import reducer from '../trends';
|
||||
|
||||
describe('trends reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
expect(reducer(undefined, {}).toJS()).toEqual({
|
||||
items: [],
|
||||
isLoading: false,
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Record as ImmutableRecord } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
ACCOUNT_NOTE_INIT_MODAL,
|
||||
|
@ -9,10 +8,12 @@ import {
|
|||
ACCOUNT_NOTE_SUBMIT_SUCCESS,
|
||||
} from '../actions/account-notes';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const EditRecord = ImmutableRecord({
|
||||
isSubmitting: false,
|
||||
account: null,
|
||||
comment: null,
|
||||
comment: '',
|
||||
});
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
|
@ -26,7 +27,7 @@ export default function account_notes(state: State = ReducerRecord(), action: An
|
|||
case ACCOUNT_NOTE_INIT_MODAL:
|
||||
return state.withMutations((state) => {
|
||||
state.setIn(['edit', 'isSubmitting'], false);
|
||||
state.setIn(['edit', 'account_id'], action.account.get('id'));
|
||||
state.setIn(['edit', 'account'], action.account.get('id'));
|
||||
state.setIn(['edit', 'comment'], action.comment);
|
||||
});
|
||||
case ACCOUNT_NOTE_CHANGE_COMMENT:
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
OrderedSet as ImmutableOrderedSet,
|
||||
fromJS,
|
||||
} from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
ADMIN_USERS_FETCH_SUCCESS,
|
||||
|
@ -39,6 +38,8 @@ import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
|||
import { normalizeAccount } from 'soapbox/normalizers/account';
|
||||
import { normalizeId } from 'soapbox/utils/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type AccountRecord = ReturnType<typeof normalizeAccount>;
|
||||
type AccountMap = ImmutableMap<string, any>;
|
||||
type APIEntity = Record<string, any>;
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
import type { AnyAction } from 'redux';
|
||||
import type { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
aliases: ImmutableRecord({
|
||||
items: ImmutableList<string>(),
|
||||
|
|
|
@ -257,7 +257,6 @@ const importMastodonPreload = (state, data) => {
|
|||
});
|
||||
};
|
||||
|
||||
|
||||
const persistAuthAccount = account => {
|
||||
if (account && account.url) {
|
||||
const key = `authAccount:${account.url}`;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
CHATS_FETCH_SUCCESS,
|
||||
|
@ -11,6 +10,8 @@ import {
|
|||
} from 'soapbox/actions/chats';
|
||||
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type APIEntity = Record<string, any>;
|
||||
type APIEntities = Array<APIEntity>;
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Map as ImmutableMap, fromJS } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
CHATS_FETCH_SUCCESS,
|
||||
|
@ -13,6 +12,8 @@ import {
|
|||
import { STREAMING_CHAT_UPDATE } from 'soapbox/actions/streaming';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type ChatMessageRecord = ReturnType<typeof normalizeChatMessage>;
|
||||
type APIEntity = Record<string, any>;
|
||||
type APIEntities = Array<APIEntity>;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { HISTORY_FETCH_REQUEST, HISTORY_FETCH_SUCCESS, HISTORY_FETCH_FAIL } from 'soapbox/actions/history';
|
||||
import { normalizeStatusEdit } from 'soapbox/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type StatusEditRecord = ReturnType<typeof normalizeStatusEdit>;
|
||||
|
||||
const HistoryRecord = ImmutableRecord({
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { ADMIN_CONFIG_UPDATE_REQUEST, ADMIN_CONFIG_UPDATE_SUCCESS } from 'soapbox/actions/admin';
|
||||
import { PLEROMA_PRELOAD_IMPORT } from 'soapbox/actions/preload';
|
||||
|
@ -13,6 +12,8 @@ import {
|
|||
fetchNodeinfo,
|
||||
} from '../actions/instance';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const initialState = normalizeInstance(ImmutableMap());
|
||||
|
||||
const nodeinfoToInstance = (nodeinfo: ImmutableMap<string, any>) => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
LIST_ADDER_RESET,
|
||||
|
@ -11,6 +10,8 @@ import {
|
|||
LIST_EDITOR_REMOVE_SUCCESS,
|
||||
} from '../actions/lists';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const ListsRecord = ImmutableRecord({
|
||||
items: ImmutableList<string>(),
|
||||
loaded: false,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
LIST_CREATE_REQUEST,
|
||||
|
@ -21,6 +20,8 @@ import {
|
|||
LIST_EDITOR_REMOVE_SUCCESS,
|
||||
} from '../actions/lists';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const AccountsRecord = ImmutableRecord({
|
||||
items: ImmutableList<string>(),
|
||||
loaded: false,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
LIST_FETCH_SUCCESS,
|
||||
|
@ -11,6 +10,8 @@ import {
|
|||
} from 'soapbox/actions/lists';
|
||||
import { normalizeList } from 'soapbox/normalizers';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
type ListRecord = ReturnType<typeof normalizeList>;
|
||||
type APIEntity = Record<string, any>;
|
||||
type APIEntities = Array<APIEntity>;
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modals';
|
||||
|
||||
const initialState = ImmutableList();
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
export default function modal(state = initialState, action) {
|
||||
const ModalRecord = ImmutableRecord({
|
||||
modalType: '',
|
||||
modalProps: null as Record<string, any> | null,
|
||||
});
|
||||
|
||||
type Modal = ReturnType<typeof ModalRecord>;
|
||||
type State = ImmutableList<Modal>;
|
||||
|
||||
export default function modal(state: State = ImmutableList<Modal>(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case MODAL_OPEN:
|
||||
return state.push({ modalType: action.modalType, modalProps: action.modalProps });
|
||||
return state.push(ModalRecord({ modalType: action.modalType, modalProps: action.modalProps }));
|
||||
case MODAL_CLOSE:
|
||||
if (state.size === 0) {
|
||||
return state;
|
|
@ -1,4 +1,4 @@
|
|||
import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable';
|
||||
import { Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||
|
||||
import {
|
||||
REPORT_INIT,
|
||||
|
@ -13,39 +13,45 @@ import {
|
|||
REPORT_RULE_CHANGE,
|
||||
} from '../actions/reports';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
new: ImmutableMap({
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const NewReportRecord = ImmutableRecord({
|
||||
isSubmitting: false,
|
||||
account_id: null,
|
||||
status_ids: ImmutableSet(),
|
||||
account_id: null as string | null,
|
||||
status_ids: ImmutableSet<string>(),
|
||||
comment: '',
|
||||
forward: false,
|
||||
block: false,
|
||||
rule_ids: ImmutableSet(),
|
||||
}),
|
||||
rule_ids: ImmutableSet<string>(),
|
||||
});
|
||||
|
||||
export default function reports(state = initialState, action) {
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
new: NewReportRecord(),
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
export default function reports(state: State = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case REPORT_INIT:
|
||||
return state.withMutations(map => {
|
||||
map.setIn(['new', 'isSubmitting'], false);
|
||||
map.setIn(['new', 'account_id'], action.account.get('id'));
|
||||
map.setIn(['new', 'account_id'], action.account.id);
|
||||
|
||||
if (state.getIn(['new', 'account_id']) !== action.account.get('id')) {
|
||||
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet());
|
||||
if (state.new.account_id !== action.account.id) {
|
||||
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.reblog?.id || action.status.id]) : ImmutableSet());
|
||||
map.setIn(['new', 'comment'], '');
|
||||
} else if (action.status) {
|
||||
map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id'))));
|
||||
map.updateIn(['new', 'status_ids'], set => (set as ImmutableSet<string>).add(action.status.reblog?.id || action.status.id));
|
||||
}
|
||||
});
|
||||
case REPORT_STATUS_TOGGLE:
|
||||
return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => {
|
||||
return state.updateIn(['new', 'status_ids'], set => {
|
||||
if (action.checked) {
|
||||
return set.add(action.statusId);
|
||||
return (set as ImmutableSet<string>).add(action.statusId);
|
||||
}
|
||||
|
||||
return set.remove(action.statusId);
|
||||
return (set as ImmutableSet<string>).remove(action.statusId);
|
||||
});
|
||||
case REPORT_COMMENT_CHANGE:
|
||||
return state.setIn(['new', 'comment'], action.comment);
|
||||
|
@ -54,12 +60,12 @@ export default function reports(state = initialState, action) {
|
|||
case REPORT_BLOCK_CHANGE:
|
||||
return state.setIn(['new', 'block'], action.block);
|
||||
case REPORT_RULE_CHANGE:
|
||||
return state.updateIn(['new', 'rule_ids'], ImmutableSet(), (set) => {
|
||||
if (set.includes(action.rule_id)) {
|
||||
return set.remove(action.rule_id);
|
||||
return state.updateIn(['new', 'rule_ids'], (set) => {
|
||||
if ((set as ImmutableSet<string>).includes(action.rule_id)) {
|
||||
return (set as ImmutableSet<string>).remove(action.rule_id);
|
||||
}
|
||||
|
||||
return set.add(action.rule_id);
|
||||
return (set as ImmutableSet<string>).add(action.rule_id);
|
||||
});
|
||||
case REPORT_SUBMIT_REQUEST:
|
||||
return state.setIn(['new', 'isSubmitting'], true);
|
|
@ -1,4 +1,6 @@
|
|||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
import { normalizeTag } from 'soapbox/normalizers';
|
||||
|
||||
import {
|
||||
COMPOSE_MENTION,
|
||||
|
@ -17,26 +19,45 @@ import {
|
|||
SEARCH_EXPAND_SUCCESS,
|
||||
} from '../actions/search';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { APIEntity, Tag } from 'soapbox/types/entities';
|
||||
|
||||
const ResultsRecord = ImmutableRecord({
|
||||
accounts: ImmutableOrderedSet<string>(),
|
||||
statuses: ImmutableOrderedSet<string>(),
|
||||
hashtags: ImmutableOrderedSet<Tag>(), // it's a list of maps
|
||||
accountsHasMore: false,
|
||||
statusesHasMore: false,
|
||||
hashtagsHasMore: false,
|
||||
accountsLoaded: false,
|
||||
statusesLoaded: false,
|
||||
hashtagsLoaded: false,
|
||||
});
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
value: '',
|
||||
submitted: false,
|
||||
submittedValue: '',
|
||||
hidden: false,
|
||||
results: ImmutableMap(),
|
||||
filter: 'accounts',
|
||||
results: ResultsRecord(),
|
||||
filter: 'accounts' as SearchFilter,
|
||||
});
|
||||
|
||||
const toIds = items => {
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
type APIEntities = Array<APIEntity>;
|
||||
export type SearchFilter = 'accounts' | 'statuses' | 'hashtags';
|
||||
|
||||
const toIds = (items: APIEntities) => {
|
||||
return ImmutableOrderedSet(items.map(item => item.id));
|
||||
};
|
||||
|
||||
const importResults = (state, results, searchTerm, searchType) => {
|
||||
const importResults = (state: State, results: APIEntity, searchTerm: string, searchType: SearchFilter) => {
|
||||
return state.withMutations(state => {
|
||||
if (state.get('value') === searchTerm && state.get('filter') === searchType) {
|
||||
state.set('results', ImmutableMap({
|
||||
if (state.value === searchTerm && state.filter === searchType) {
|
||||
state.set('results', ResultsRecord({
|
||||
accounts: toIds(results.accounts),
|
||||
statuses: toIds(results.statuses),
|
||||
hashtags: fromJS(results.hashtags), // it's a list of maps
|
||||
hashtags: ImmutableOrderedSet(results.hashtags.map(normalizeTag)), // it's a list of records
|
||||
accountsHasMore: results.accounts.length >= 20,
|
||||
statusesHasMore: results.statuses.length >= 20,
|
||||
hashtagsHasMore: results.hashtags.length >= 20,
|
||||
|
@ -50,38 +71,38 @@ const importResults = (state, results, searchTerm, searchType) => {
|
|||
});
|
||||
};
|
||||
|
||||
const paginateResults = (state, searchType, results, searchTerm) => {
|
||||
const paginateResults = (state: State, searchType: SearchFilter, results: APIEntity, searchTerm: string) => {
|
||||
return state.withMutations(state => {
|
||||
if (state.get('value') === searchTerm) {
|
||||
if (state.value === searchTerm) {
|
||||
state.setIn(['results', `${searchType}HasMore`], results[searchType].length >= 20);
|
||||
state.setIn(['results', `${searchType}Loaded`], true);
|
||||
state.updateIn(['results', searchType], items => {
|
||||
const data = results[searchType];
|
||||
// Hashtags are a list of maps. Others are IDs.
|
||||
if (searchType === 'hashtags') {
|
||||
return items.concat(fromJS(data));
|
||||
return (items as ImmutableOrderedSet<string>).concat(fromJS(data));
|
||||
} else {
|
||||
return items.concat(toIds(data));
|
||||
return (items as ImmutableOrderedSet<string>).concat(toIds(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmitted = (state, value) => {
|
||||
const handleSubmitted = (state: State, value: string) => {
|
||||
return state.withMutations(state => {
|
||||
state.set('results', ImmutableMap());
|
||||
state.set('results', ResultsRecord());
|
||||
state.set('submitted', true);
|
||||
state.set('submittedValue', value);
|
||||
});
|
||||
};
|
||||
|
||||
export default function search(state = initialState, action) {
|
||||
export default function search(state = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case SEARCH_CHANGE:
|
||||
return state.set('value', action.value);
|
||||
case SEARCH_CLEAR:
|
||||
return initialState;
|
||||
return ReducerRecord();
|
||||
case SEARCH_SHOW:
|
||||
return state.set('hidden', false);
|
||||
case COMPOSE_REPLY:
|
|
@ -1,5 +1,4 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import {
|
||||
MFA_FETCH_SUCCESS,
|
||||
|
@ -11,6 +10,8 @@ import {
|
|||
REVOKE_TOKEN_SUCCESS,
|
||||
} from '../actions/security';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const TokenRecord = ImmutableRecord({
|
||||
id: 0,
|
||||
app_name: '',
|
|
@ -1,6 +1,5 @@
|
|||
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
|
||||
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'soapbox/actions/accounts';
|
||||
import { DOMAIN_BLOCK_SUCCESS } from 'soapbox/actions/domain_blocks';
|
||||
import {
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
|
||||
import {
|
||||
TRENDING_STATUSES_FETCH_REQUEST,
|
||||
TRENDING_STATUSES_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/trending_statuses';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
items: ImmutableOrderedSet(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const toIds = items => ImmutableOrderedSet(items.map(item => item.id));
|
||||
|
||||
const importStatuses = (state, statuses) => {
|
||||
return state.withMutations(state => {
|
||||
state.set('items', toIds(statuses));
|
||||
state.set('isLoading', false);
|
||||
});
|
||||
};
|
||||
|
||||
export default function trending_statuses(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case TRENDING_STATUSES_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case TRENDING_STATUSES_FETCH_SUCCESS:
|
||||
return importStatuses(state, action.statuses);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
import { OrderedSet as ImmutableOrderedSet, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
TRENDING_STATUSES_FETCH_REQUEST,
|
||||
TRENDING_STATUSES_FETCH_SUCCESS,
|
||||
} from 'soapbox/actions/trending_statuses';
|
||||
import { APIEntity } from 'soapbox/types/entities';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
items: ImmutableOrderedSet<string>(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
type APIEntities = Array<APIEntity>;
|
||||
|
||||
const toIds = (items: APIEntities) => ImmutableOrderedSet(items.map(item => item.id));
|
||||
|
||||
const importStatuses = (state: State, statuses: APIEntities) => {
|
||||
return state.withMutations(state => {
|
||||
state.set('items', toIds(statuses));
|
||||
state.set('isLoading', false);
|
||||
});
|
||||
};
|
||||
|
||||
export default function trending_statuses(state: State = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case TRENDING_STATUSES_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case TRENDING_STATUSES_FETCH_SUCCESS:
|
||||
return importStatuses(state, action.statuses);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
|
||||
|
||||
import {
|
||||
TRENDS_FETCH_REQUEST,
|
||||
TRENDS_FETCH_SUCCESS,
|
||||
TRENDS_FETCH_FAIL,
|
||||
} from '../actions/trends';
|
||||
|
||||
const initialState = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
export default function trendsReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case TRENDS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case TRENDS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('items', fromJS(action.tags.map((x => x))));
|
||||
map.set('isLoading', false);
|
||||
});
|
||||
case TRENDS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import { List as ImmutableList, Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import { normalizeTag } from 'soapbox/normalizers';
|
||||
|
||||
import {
|
||||
TRENDS_FETCH_REQUEST,
|
||||
TRENDS_FETCH_SUCCESS,
|
||||
TRENDS_FETCH_FAIL,
|
||||
} from '../actions/trends';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { APIEntity, Tag } from 'soapbox/types/entities';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
items: ImmutableList<Tag>(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
export default function trendsReducer(state: State = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case TRENDS_FETCH_REQUEST:
|
||||
return state.set('isLoading', true);
|
||||
case TRENDS_FETCH_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('items', ImmutableList(action.tags.map((item: APIEntity) => normalizeTag(item))));
|
||||
map.set('isLoading', false);
|
||||
});
|
||||
case TRENDS_FETCH_FAIL:
|
||||
return state.set('isLoading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue