mirror of
https://github.com/mastodon/mastodon.git
synced 2025-09-06 09:51:24 +00:00
Change order of items in navigation panel in web UI (#35029)
This commit is contained in:
parent
013c527406
commit
7c4393e719
|
@ -4,14 +4,12 @@ export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
|
||||||
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
|
||||||
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
export const LIST_FETCH_FAIL = 'LIST_FETCH_FAIL';
|
||||||
|
|
||||||
export const LISTS_FETCH_REQUEST = 'LISTS_FETCH_REQUEST';
|
|
||||||
export const LISTS_FETCH_SUCCESS = 'LISTS_FETCH_SUCCESS';
|
|
||||||
export const LISTS_FETCH_FAIL = 'LISTS_FETCH_FAIL';
|
|
||||||
|
|
||||||
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
export const LIST_DELETE_REQUEST = 'LIST_DELETE_REQUEST';
|
||||||
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
export const LIST_DELETE_SUCCESS = 'LIST_DELETE_SUCCESS';
|
||||||
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
export const LIST_DELETE_FAIL = 'LIST_DELETE_FAIL';
|
||||||
|
|
||||||
|
export * from './lists_typed';
|
||||||
|
|
||||||
export const fetchList = id => (dispatch, getState) => {
|
export const fetchList = id => (dispatch, getState) => {
|
||||||
if (getState().getIn(['lists', id])) {
|
if (getState().getIn(['lists', id])) {
|
||||||
return;
|
return;
|
||||||
|
@ -40,28 +38,6 @@ export const fetchListFail = (id, error) => ({
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const fetchLists = () => (dispatch) => {
|
|
||||||
dispatch(fetchListsRequest());
|
|
||||||
|
|
||||||
api().get('/api/v1/lists')
|
|
||||||
.then(({ data }) => dispatch(fetchListsSuccess(data)))
|
|
||||||
.catch(err => dispatch(fetchListsFail(err)));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchListsRequest = () => ({
|
|
||||||
type: LISTS_FETCH_REQUEST,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchListsSuccess = lists => ({
|
|
||||||
type: LISTS_FETCH_SUCCESS,
|
|
||||||
lists,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const fetchListsFail = error => ({
|
|
||||||
type: LISTS_FETCH_FAIL,
|
|
||||||
error,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteList = id => (dispatch) => {
|
export const deleteList = id => (dispatch) => {
|
||||||
dispatch(deleteListRequest(id));
|
dispatch(deleteListRequest(id));
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { apiCreate, apiUpdate } from 'mastodon/api/lists';
|
import { apiCreate, apiUpdate, apiGetLists } from 'mastodon/api/lists';
|
||||||
import type { List } from 'mastodon/models/list';
|
import type { List } from 'mastodon/models/list';
|
||||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
|
@ -11,3 +11,7 @@ export const updateList = createDataLoadingThunk(
|
||||||
'list/update',
|
'list/update',
|
||||||
(list: Partial<List>) => apiUpdate(list),
|
(list: Partial<List>) => apiUpdate(list),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const fetchLists = createDataLoadingThunk('lists/fetch', () =>
|
||||||
|
apiGetLists(),
|
||||||
|
);
|
||||||
|
|
|
@ -13,6 +13,8 @@ export const apiCreate = (list: Partial<ApiListJSON>) =>
|
||||||
export const apiUpdate = (list: Partial<ApiListJSON>) =>
|
export const apiUpdate = (list: Partial<ApiListJSON>) =>
|
||||||
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
|
apiRequestPut<ApiListJSON>(`v1/lists/${list.id}`, list);
|
||||||
|
|
||||||
|
export const apiGetLists = () => apiRequestGet<ApiListJSON[]>('v1/lists');
|
||||||
|
|
||||||
export const apiGetAccounts = (listId: string) =>
|
export const apiGetAccounts = (listId: string) =>
|
||||||
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
|
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
|
||||||
limit: 0,
|
limit: 0,
|
||||||
|
|
|
@ -16,10 +16,11 @@ export const apiFeatureTag = (tagId: string) =>
|
||||||
export const apiUnfeatureTag = (tagId: string) =>
|
export const apiUnfeatureTag = (tagId: string) =>
|
||||||
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfeature`);
|
apiRequestPost<ApiHashtagJSON>(`v1/tags/${tagId}/unfeature`);
|
||||||
|
|
||||||
export const apiGetFollowedTags = async (url?: string) => {
|
export const apiGetFollowedTags = async (url?: string, limit?: number) => {
|
||||||
const response = await api().request<ApiHashtagJSON[]>({
|
const response = await api().request<ApiHashtagJSON[]>({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: url ?? '/api/v1/followed_tags',
|
url: url ?? '/api/v1/followed_tags',
|
||||||
|
params: { limit },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
|
||||||
import { showTrends } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
export const NavigationPortal: React.FC = () => (
|
|
||||||
<div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
|
|
||||||
);
|
|
|
@ -1,54 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
|
||||||
|
|
||||||
export default class Trends extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
trends: ImmutablePropTypes.list,
|
|
||||||
fetchTrends: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.props.fetchTrends();
|
|
||||||
this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { trends } = this.props;
|
|
||||||
|
|
||||||
if (!trends || trends.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='getting-started__trends'>
|
|
||||||
<h4>
|
|
||||||
<Link to={'/explore/tags'}>
|
|
||||||
<FormattedMessage id='trends.trending_now' defaultMessage='Trending now' />
|
|
||||||
</Link>
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
|
||||||
|
|
||||||
import Trends from '../components/trends';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
trends: state.getIn(['trends', 'tags', 'items']),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
fetchTrends: () => dispatch(fetchTrendingHashtags()),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Trends);
|
|
|
@ -34,7 +34,7 @@ import { NavigationBar } from '../compose/components/navigation_bar';
|
||||||
import { ColumnLink } from '../ui/components/column_link';
|
import { ColumnLink } from '../ui/components/column_link';
|
||||||
import ColumnSubheading from '../ui/components/column_subheading';
|
import ColumnSubheading from '../ui/components/column_subheading';
|
||||||
|
|
||||||
import TrendsContainer from './containers/trends_container';
|
import { Trends } from 'mastodon/features/navigation_panel/components/trends';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
@ -164,7 +164,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
<LinkFooter multiColumn />
|
<LinkFooter multiColumn />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(multiColumn && showTrends) && <TrendsContainer />}
|
{(multiColumn && showTrends) && <Trends />}
|
||||||
|
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{intl.formatMessage(messages.menu)}</title>
|
<title>{intl.formatMessage(messages.menu)}</title>
|
||||||
|
|
|
@ -122,7 +122,7 @@ const ListAdder: React.FC<{
|
||||||
const [listIds, setListIds] = useState<string[]>([]);
|
const [listIds, setListIds] = useState<string[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchLists());
|
void dispatch(fetchLists());
|
||||||
|
|
||||||
apiGetAccountLists(accountId)
|
apiGetAccountLists(accountId)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
|
|
|
@ -79,7 +79,7 @@ const Lists: React.FC<{
|
||||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(fetchLists());
|
void dispatch(fetchLists());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const emptyMessage = (
|
const emptyMessage = (
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
import { useState, useCallback, useId } from 'react';
|
||||||
|
|
||||||
|
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
|
||||||
|
import KeyboardArrowUpIcon from '@/material-icons/400-24px/keyboard_arrow_up.svg?react';
|
||||||
|
import type { IconProp } from 'mastodon/components/icon';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
||||||
|
|
||||||
|
export const CollapsiblePanel: React.FC<{
|
||||||
|
children: React.ReactNode[];
|
||||||
|
to: string;
|
||||||
|
title: string;
|
||||||
|
collapseTitle: string;
|
||||||
|
expandTitle: string;
|
||||||
|
icon: string;
|
||||||
|
iconComponent: IconProp;
|
||||||
|
activeIconComponent?: IconProp;
|
||||||
|
loading?: boolean;
|
||||||
|
}> = ({
|
||||||
|
children,
|
||||||
|
to,
|
||||||
|
icon,
|
||||||
|
iconComponent,
|
||||||
|
activeIconComponent,
|
||||||
|
title,
|
||||||
|
collapseTitle,
|
||||||
|
expandTitle,
|
||||||
|
loading,
|
||||||
|
}) => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const accessibilityId = useId();
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
setExpanded((value) => !value);
|
||||||
|
}, [setExpanded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='navigation-panel__list-panel'>
|
||||||
|
<div className='navigation-panel__list-panel__header'>
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to={to}
|
||||||
|
icon={icon}
|
||||||
|
iconComponent={iconComponent}
|
||||||
|
activeIconComponent={activeIconComponent}
|
||||||
|
text={title}
|
||||||
|
id={`${accessibilityId}-title`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(loading || children.length > 0) && (
|
||||||
|
<>
|
||||||
|
<div className='navigation-panel__list-panel__header__sep' />
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
icon='down'
|
||||||
|
expanded={expanded}
|
||||||
|
iconComponent={
|
||||||
|
loading
|
||||||
|
? LoadingIndicator
|
||||||
|
: expanded
|
||||||
|
? KeyboardArrowUpIcon
|
||||||
|
: KeyboardArrowDownIcon
|
||||||
|
}
|
||||||
|
title={expanded ? collapseTitle : expandTitle}
|
||||||
|
onClick={handleClick}
|
||||||
|
aria-controls={`${accessibilityId}-content`}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{children.length > 0 && expanded && (
|
||||||
|
<div
|
||||||
|
className='navigation-panel__list-panel__items'
|
||||||
|
role='region'
|
||||||
|
id={`${accessibilityId}-content`}
|
||||||
|
aria-labelledby={`${accessibilityId}-title`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,88 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import {
|
||||||
|
disabledAccountId,
|
||||||
|
movedToAccountId,
|
||||||
|
domain,
|
||||||
|
} from 'mastodon/initial_state';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const DisabledAccountBanner: React.FC = () => {
|
||||||
|
const disabledAccount = useAppSelector((state) =>
|
||||||
|
disabledAccountId ? state.accounts.get(disabledAccountId) : undefined,
|
||||||
|
);
|
||||||
|
const movedToAccount = useAppSelector((state) =>
|
||||||
|
movedToAccountId ? state.accounts.get(movedToAccountId) : undefined,
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const handleLogOutClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT', modalProps: {} }));
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const disabledAccountLink = (
|
||||||
|
<Link to={`/@${disabledAccount?.acct}`}>
|
||||||
|
{disabledAccount?.acct}@{domain}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='sign-in-banner'>
|
||||||
|
<p>
|
||||||
|
{movedToAccount ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='moved_to_account_banner.text'
|
||||||
|
defaultMessage='Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.'
|
||||||
|
values={{
|
||||||
|
disabledAccount: disabledAccountLink,
|
||||||
|
movedToAccount: (
|
||||||
|
<Link to={`/@${movedToAccount.acct}`}>
|
||||||
|
{movedToAccount.acct.includes('@')
|
||||||
|
? movedToAccount.acct
|
||||||
|
: `${movedToAccount.acct}@${domain}`}
|
||||||
|
</Link>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='disabled_account_banner.text'
|
||||||
|
defaultMessage='Your account {disabledAccount} is currently disabled.'
|
||||||
|
values={{
|
||||||
|
disabledAccount: disabledAccountLink,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<a href='/auth/edit' className='button button--block'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='disabled_account_banner.account_settings'
|
||||||
|
defaultMessage='Account settings'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='button button--block button-tertiary'
|
||||||
|
onClick={handleLogOutClick}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.logout.confirm'
|
||||||
|
defaultMessage='Log out'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import TagIcon from '@/material-icons/400-24px/tag.svg?react';
|
||||||
|
import { apiGetFollowedTags } from 'mastodon/api/tags';
|
||||||
|
import type { ApiHashtagJSON } from 'mastodon/api_types/tags';
|
||||||
|
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
||||||
|
|
||||||
|
import { CollapsiblePanel } from './collapsible_panel';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
followedTags: {
|
||||||
|
id: 'navigation_bar.followed_tags',
|
||||||
|
defaultMessage: 'Followed hashtags',
|
||||||
|
},
|
||||||
|
expand: {
|
||||||
|
id: 'navigation_panel.expand_followed_tags',
|
||||||
|
defaultMessage: 'Expand followed hashtags menu',
|
||||||
|
},
|
||||||
|
collapse: {
|
||||||
|
id: 'navigation_panel.collapse_followed_tags',
|
||||||
|
defaultMessage: 'Collapse followed hashtags menu',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FollowedTagsPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [tags, setTags] = useState<ApiHashtagJSON[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void apiGetFollowedTags(undefined, 4)
|
||||||
|
.then(({ tags }) => {
|
||||||
|
setTags(tags);
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [setLoading, setTags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsiblePanel
|
||||||
|
to='/followed_tags'
|
||||||
|
icon='hashtag'
|
||||||
|
iconComponent={TagIcon}
|
||||||
|
title={intl.formatMessage(messages.followedTags)}
|
||||||
|
collapseTitle={intl.formatMessage(messages.collapse)}
|
||||||
|
expandTitle={intl.formatMessage(messages.expand)}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<ColumnLink
|
||||||
|
icon='hashtag'
|
||||||
|
key={tag.name}
|
||||||
|
iconComponent={TagIcon}
|
||||||
|
text={`#${tag.name}`}
|
||||||
|
to={`/tags/${tag.name}`}
|
||||||
|
transparent
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CollapsiblePanel>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
|
||||||
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
|
import { fetchLists } from 'mastodon/actions/lists';
|
||||||
|
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
||||||
|
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { CollapsiblePanel } from './collapsible_panel';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||||
|
expand: {
|
||||||
|
id: 'navigation_panel.expand_lists',
|
||||||
|
defaultMessage: 'Expand list menu',
|
||||||
|
},
|
||||||
|
collapse: {
|
||||||
|
id: 'navigation_panel.collapse_lists',
|
||||||
|
defaultMessage: 'Collapse list menu',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListPanel: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
void dispatch(fetchLists()).then(() => {
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
}, [dispatch, setLoading]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollapsiblePanel
|
||||||
|
to='/lists'
|
||||||
|
icon='list-ul'
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
activeIconComponent={ListAltActiveIcon}
|
||||||
|
title={intl.formatMessage(messages.lists)}
|
||||||
|
collapseTitle={intl.formatMessage(messages.collapse)}
|
||||||
|
expandTitle={intl.formatMessage(messages.expand)}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{lists.map((list) => (
|
||||||
|
<ColumnLink
|
||||||
|
icon='list-ul'
|
||||||
|
key={list.id}
|
||||||
|
iconComponent={ListAltIcon}
|
||||||
|
activeIconComponent={ListAltActiveIcon}
|
||||||
|
text={list.title}
|
||||||
|
to={`/lists/${list.id}`}
|
||||||
|
transparent
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</CollapsiblePanel>
|
||||||
|
);
|
||||||
|
};
|
|
@ -12,10 +12,6 @@ import { canManageReports, canViewAdminDashboard } from 'mastodon/permissions';
|
||||||
import { useAppDispatch } from 'mastodon/store';
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
followedTags: {
|
|
||||||
id: 'navigation_bar.followed_tags',
|
|
||||||
defaultMessage: 'Followed hashtags',
|
|
||||||
},
|
|
||||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||||
domainBlocks: {
|
domainBlocks: {
|
||||||
id: 'navigation_bar.domain_blocks',
|
id: 'navigation_bar.domain_blocks',
|
||||||
|
@ -54,11 +50,6 @@ export const MoreLink: React.FC = () => {
|
||||||
|
|
||||||
const menu = useMemo(() => {
|
const menu = useMemo(() => {
|
||||||
const arr: MenuItem[] = [
|
const arr: MenuItem[] = [
|
||||||
{
|
|
||||||
text: intl.formatMessage(messages.followedTags),
|
|
||||||
to: '/followed_tags',
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{ text: intl.formatMessage(messages.filters), href: '/filters' },
|
{ text: intl.formatMessage(messages.filters), href: '/filters' },
|
||||||
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
|
{ text: intl.formatMessage(messages.mutes), to: '/mutes' },
|
||||||
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
|
{ text: intl.formatMessage(messages.blocks), to: '/blocks' },
|
|
@ -0,0 +1,105 @@
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const SignInBanner: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const openClosedRegistrationsModal = useCallback(
|
||||||
|
() =>
|
||||||
|
dispatch(
|
||||||
|
openModal({ modalType: 'CLOSED_REGISTRATIONS', modalProps: {} }),
|
||||||
|
),
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
let signupButton: React.ReactNode;
|
||||||
|
|
||||||
|
const signupUrl = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.server.getIn(['server', 'registrations', 'url'], null) as
|
||||||
|
| string
|
||||||
|
| null) ?? '/auth/sign_up',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sso_redirect) {
|
||||||
|
return (
|
||||||
|
<div className='sign-in-banner'>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.mastodon_is'
|
||||||
|
defaultMessage="Mastodon is the best way to keep up with what's happening."
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.follow_anyone'
|
||||||
|
defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={sso_redirect}
|
||||||
|
data-method='post'
|
||||||
|
className='button button--block button-tertiary'
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.sso_redirect'
|
||||||
|
defaultMessage='Login or Register'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registrationsOpen) {
|
||||||
|
signupButton = (
|
||||||
|
<a href={signupUrl} className='button button--block'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.create_account'
|
||||||
|
defaultMessage='Create account'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
signupButton = (
|
||||||
|
<button
|
||||||
|
className='button button--block'
|
||||||
|
onClick={openClosedRegistrationsModal}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.create_account'
|
||||||
|
defaultMessage='Create account'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='sign-in-banner'>
|
||||||
|
<p>
|
||||||
|
<strong>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.mastodon_is'
|
||||||
|
defaultMessage="Mastodon is the best way to keep up with what's happening."
|
||||||
|
/>
|
||||||
|
</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<FormattedMessage
|
||||||
|
id='sign_in_banner.follow_anyone'
|
||||||
|
defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.'
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
{signupButton}
|
||||||
|
<a href='/auth/sign_in' className='button button--block button-tertiary'>
|
||||||
|
<FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { fetchTrendingHashtags } from 'mastodon/actions/trends';
|
||||||
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
import { showTrends } from 'mastodon/initial_state';
|
||||||
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const Trends: React.FC = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const trends = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.trends.getIn(['tags', 'items']) as ImmutableList<
|
||||||
|
ImmutableMap<string, unknown>
|
||||||
|
>,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchTrendingHashtags());
|
||||||
|
|
||||||
|
const refreshInterval = setInterval(() => {
|
||||||
|
dispatch(fetchTrendingHashtags());
|
||||||
|
}, 900 * 1000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(refreshInterval);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
if (!showTrends || trends.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='navigation-panel__portal'>
|
||||||
|
<div className='getting-started__trends'>
|
||||||
|
<h4>
|
||||||
|
<Link to={'/explore/tags'}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='trends.trending_now'
|
||||||
|
defaultMessage='Trending now'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{trends.take(4).map((hashtag) => (
|
||||||
|
<Hashtag key={hashtag.get('name') as string} hashtag={hashtag} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
@ -22,7 +22,6 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
|
||||||
import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
import PersonAddActiveIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||||
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
|
||||||
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
import SettingsIcon from '@/material-icons/400-24px/settings.svg?react';
|
||||||
import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
import StarActiveIcon from '@/material-icons/400-24px/star-fill.svg?react';
|
||||||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||||
|
@ -32,7 +31,8 @@ import { openNavigation, closeNavigation } from 'mastodon/actions/navigation';
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
import { IconWithBadge } from 'mastodon/components/icon_with_badge';
|
||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
import { NavigationPortal } from 'mastodon/components/navigation_portal';
|
import { Search } from 'mastodon/features/compose/components/search';
|
||||||
|
import { ColumnLink } from 'mastodon/features/ui/components/column_link';
|
||||||
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
import { useBreakpoint } from 'mastodon/features/ui/hooks/useBreakpoint';
|
||||||
import { useIdentity } from 'mastodon/identity_context';
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state';
|
import { timelinePreview, trendsEnabled, me } from 'mastodon/initial_state';
|
||||||
|
@ -40,11 +40,12 @@ import { transientSingleColumn } from 'mastodon/is_mobile';
|
||||||
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifications';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
import { ColumnLink } from './column_link';
|
import { DisabledAccountBanner } from './components/disabled_account_banner';
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
import { FollowedTagsPanel } from './components/followed_tags_panel';
|
||||||
import { ListPanel } from './list_panel';
|
import { ListPanel } from './components/list_panel';
|
||||||
import { MoreLink } from './more_link';
|
import { MoreLink } from './components/more_link';
|
||||||
import SignInBanner from './sign_in_banner';
|
import { SignInBanner } from './components/sign_in_banner';
|
||||||
|
import { Trends } from './components/trends';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
home: { id: 'tabs_bar.home', defaultMessage: 'Home' },
|
||||||
|
@ -67,6 +68,10 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
searchTrends: {
|
||||||
|
id: 'navigation_bar.search_trends',
|
||||||
|
defaultMessage: 'Search / Trending',
|
||||||
|
},
|
||||||
advancedInterface: {
|
advancedInterface: {
|
||||||
id: 'navigation_bar.advanced_interface',
|
id: 'navigation_bar.advanced_interface',
|
||||||
defaultMessage: 'Open in advanced web interface',
|
defaultMessage: 'Open in advanced web interface',
|
||||||
|
@ -159,33 +164,6 @@ const FollowRequestsLink: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchLink: React.FC = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const showAsSearch = useBreakpoint('full');
|
|
||||||
|
|
||||||
if (!trendsEnabled || showAsSearch) {
|
|
||||||
return (
|
|
||||||
<ColumnLink
|
|
||||||
transparent
|
|
||||||
to={trendsEnabled ? '/explore' : '/search'}
|
|
||||||
icon='search'
|
|
||||||
iconComponent={SearchIcon}
|
|
||||||
text={intl.formatMessage(messages.search)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ColumnLink
|
|
||||||
transparent
|
|
||||||
to='/explore'
|
|
||||||
icon='explore'
|
|
||||||
iconComponent={TrendingUpIcon}
|
|
||||||
text={intl.formatMessage(messages.explore)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProfileCard: React.FC = () => {
|
const ProfileCard: React.FC = () => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -198,6 +176,13 @@ const ProfileCard: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isFirehoseActive = (
|
||||||
|
match: unknown,
|
||||||
|
{ pathname }: { pathname: string },
|
||||||
|
) => {
|
||||||
|
return !!match || pathname.startsWith('/public');
|
||||||
|
};
|
||||||
|
|
||||||
const MENU_WIDTH = 284;
|
const MENU_WIDTH = 284;
|
||||||
|
|
||||||
export const NavigationPanel: React.FC = () => {
|
export const NavigationPanel: React.FC = () => {
|
||||||
|
@ -206,6 +191,7 @@ export const NavigationPanel: React.FC = () => {
|
||||||
const open = useAppSelector((state) => state.navigation.open);
|
const open = useAppSelector((state) => state.navigation.open);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const openable = useBreakpoint('openable');
|
const openable = useBreakpoint('openable');
|
||||||
|
const showSearch = useBreakpoint('full');
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const overlayRef = useRef<HTMLDivElement | null>(null);
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
@ -275,13 +261,6 @@ export const NavigationPanel: React.FC = () => {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFirehoseActive = useCallback(
|
|
||||||
(match: unknown, location: { pathname: string }): boolean => {
|
|
||||||
return !!match || location.pathname.startsWith('/public');
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const previouslyFocusedElementRef = useRef<HTMLElement | null>();
|
const previouslyFocusedElementRef = useRef<HTMLElement | null>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -297,7 +276,7 @@ export const NavigationPanel: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
let banner = undefined;
|
let banner: React.ReactNode;
|
||||||
|
|
||||||
if (transientSingleColumn) {
|
if (transientSingleColumn) {
|
||||||
banner = (
|
banner = (
|
||||||
|
@ -335,6 +314,8 @@ export const NavigationPanel: React.FC = () => {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showSearch && <Search singleColumn />}
|
||||||
|
|
||||||
<ProfileCard />
|
<ProfileCard />
|
||||||
|
|
||||||
{banner && <div className='navigation-panel__banner'>{banner}</div>}
|
{banner && <div className='navigation-panel__banner'>{banner}</div>}
|
||||||
|
@ -358,43 +339,49 @@ export const NavigationPanel: React.FC = () => {
|
||||||
activeIconComponent={HomeActiveIcon}
|
activeIconComponent={HomeActiveIcon}
|
||||||
text={intl.formatMessage(messages.home)}
|
text={intl.formatMessage(messages.home)}
|
||||||
/>
|
/>
|
||||||
<NotificationsLink />
|
|
||||||
<FollowRequestsLink />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SearchLink />
|
{trendsEnabled && (
|
||||||
|
<ColumnLink
|
||||||
|
transparent
|
||||||
|
to='/explore'
|
||||||
|
icon='explore'
|
||||||
|
iconComponent={TrendingUpIcon}
|
||||||
|
text={intl.formatMessage(messages.explore)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{(signedIn || timelinePreview) && (
|
{(signedIn || timelinePreview) && (
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
transparent
|
transparent
|
||||||
to='/public/local'
|
to='/public/local'
|
||||||
isActive={isFirehoseActive}
|
|
||||||
icon='globe'
|
icon='globe'
|
||||||
iconComponent={PublicIcon}
|
iconComponent={PublicIcon}
|
||||||
|
isActive={isFirehoseActive}
|
||||||
text={intl.formatMessage(messages.firehose)}
|
text={intl.formatMessage(messages.firehose)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!signedIn && (
|
|
||||||
<div className='navigation-panel__sign-in-banner'>
|
|
||||||
<hr />
|
|
||||||
{disabledAccountId ? (
|
|
||||||
<DisabledAccountBanner />
|
|
||||||
) : (
|
|
||||||
<SignInBanner />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<>
|
||||||
|
<NotificationsLink />
|
||||||
|
|
||||||
|
<FollowRequestsLink />
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<ListPanel />
|
||||||
|
|
||||||
|
<FollowedTagsPanel />
|
||||||
|
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
transparent
|
transparent
|
||||||
to='/conversations'
|
to='/favourites'
|
||||||
icon='at'
|
icon='star'
|
||||||
iconComponent={AlternateEmailIcon}
|
iconComponent={StarIcon}
|
||||||
text={intl.formatMessage(messages.direct)}
|
activeIconComponent={StarActiveIcon}
|
||||||
|
text={intl.formatMessage(messages.favourites)}
|
||||||
/>
|
/>
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
transparent
|
transparent
|
||||||
|
@ -406,15 +393,12 @@ export const NavigationPanel: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
transparent
|
transparent
|
||||||
to='/favourites'
|
to='/conversations'
|
||||||
icon='star'
|
icon='at'
|
||||||
iconComponent={StarIcon}
|
iconComponent={AlternateEmailIcon}
|
||||||
activeIconComponent={StarActiveIcon}
|
text={intl.formatMessage(messages.direct)}
|
||||||
text={intl.formatMessage(messages.favourites)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListPanel />
|
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
|
@ -430,8 +414,6 @@ export const NavigationPanel: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className='navigation-panel__legal'>
|
<div className='navigation-panel__legal'>
|
||||||
<hr />
|
|
||||||
|
|
||||||
<ColumnLink
|
<ColumnLink
|
||||||
transparent
|
transparent
|
||||||
to='/about'
|
to='/about'
|
||||||
|
@ -440,11 +422,23 @@ export const NavigationPanel: React.FC = () => {
|
||||||
text={intl.formatMessage(messages.about)}
|
text={intl.formatMessage(messages.about)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!signedIn && (
|
||||||
|
<div className='navigation-panel__sign-in-banner'>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
{disabledAccountId ? (
|
||||||
|
<DisabledAccountBanner />
|
||||||
|
) : (
|
||||||
|
<SignInBanner />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-spacer' />
|
<div className='flex-spacer' />
|
||||||
|
|
||||||
<NavigationPortal />
|
<Trends />
|
||||||
</div>
|
</div>
|
||||||
</animated.div>
|
</animated.div>
|
||||||
</div>
|
</div>
|
|
@ -25,7 +25,7 @@ import BundleColumnError from './bundle_column_error';
|
||||||
import { ColumnLoading } from './column_loading';
|
import { ColumnLoading } from './column_loading';
|
||||||
import { ComposePanel } from './compose_panel';
|
import { ComposePanel } from './compose_panel';
|
||||||
import DrawerLoading from './drawer_loading';
|
import DrawerLoading from './drawer_loading';
|
||||||
import { NavigationPanel } from './navigation_panel';
|
import { NavigationPanel } from 'mastodon/features/navigation_panel';
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
'COMPOSE': Compose,
|
'COMPOSE': Compose,
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { disabledAccountId, movedToAccountId, domain } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
const mapStateToProps = (state) => ({
|
|
||||||
disabledAcct: state.getIn(['accounts', disabledAccountId, 'acct']),
|
|
||||||
movedToAcct: movedToAccountId ? state.getIn(['accounts', movedToAccountId, 'acct']) : undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
onLogout () {
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' }));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class DisabledAccountBanner extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
disabledAcct: PropTypes.string.isRequired,
|
|
||||||
movedToAcct: PropTypes.string,
|
|
||||||
onLogout: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleLogOutClick = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
this.props.onLogout();
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { disabledAcct, movedToAcct } = this.props;
|
|
||||||
|
|
||||||
const disabledAccountLink = (
|
|
||||||
<Link to={`/@${disabledAcct}`}>
|
|
||||||
{disabledAcct}@{domain}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='sign-in-banner'>
|
|
||||||
<p>
|
|
||||||
{movedToAcct ? (
|
|
||||||
<FormattedMessage
|
|
||||||
id='moved_to_account_banner.text'
|
|
||||||
defaultMessage='Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.'
|
|
||||||
values={{
|
|
||||||
disabledAccount: disabledAccountLink,
|
|
||||||
movedToAccount: <Link to={`/@${movedToAcct}`}>{movedToAcct.includes('@') ? movedToAcct : `${movedToAcct}@${domain}`}</Link>,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FormattedMessage
|
|
||||||
id='disabled_account_banner.text'
|
|
||||||
defaultMessage='Your account {disabledAccount} is currently disabled.'
|
|
||||||
values={{
|
|
||||||
disabledAccount: disabledAccountLink,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<a href='/auth/edit' className='button button--block'>
|
|
||||||
<FormattedMessage id='disabled_account_banner.account_settings' defaultMessage='Account settings' />
|
|
||||||
</a>
|
|
||||||
<button type='button' className='button button--block button-tertiary' onClick={this.handleLogOutClick}>
|
|
||||||
<FormattedMessage id='confirmations.logout.confirm' defaultMessage='Log out' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(DisabledAccountBanner));
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { useEffect, useState, useCallback, useId } from 'react';
|
|
||||||
|
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
|
||||||
|
|
||||||
import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
|
|
||||||
import ArrowLeftIcon from '@/material-icons/400-24px/arrow_left.svg?react';
|
|
||||||
import ListAltActiveIcon from '@/material-icons/400-24px/list_alt-fill.svg?react';
|
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
|
||||||
import { fetchLists } from 'mastodon/actions/lists';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { ColumnLink } from './column_link';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
|
||||||
expand: {
|
|
||||||
id: 'navigation_panel.expand_lists',
|
|
||||||
defaultMessage: 'Expand list menu',
|
|
||||||
},
|
|
||||||
collapse: {
|
|
||||||
id: 'navigation_panel.collapse_lists',
|
|
||||||
defaultMessage: 'Collapse list menu',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ListPanel: React.FC = () => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const lists = useAppSelector((state) => getOrderedLists(state));
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
|
||||||
const accessibilityId = useId();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetchLists());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
|
||||||
setExpanded((value) => !value);
|
|
||||||
}, [setExpanded]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='navigation-panel__list-panel'>
|
|
||||||
<div className='navigation-panel__list-panel__header'>
|
|
||||||
<ColumnLink
|
|
||||||
transparent
|
|
||||||
to='/lists'
|
|
||||||
icon='list-ul'
|
|
||||||
iconComponent={ListAltIcon}
|
|
||||||
activeIconComponent={ListAltActiveIcon}
|
|
||||||
text={intl.formatMessage(messages.lists)}
|
|
||||||
id={`${accessibilityId}-title`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{lists.length > 0 && (
|
|
||||||
<IconButton
|
|
||||||
icon='down'
|
|
||||||
expanded={expanded}
|
|
||||||
iconComponent={expanded ? ArrowDropDownIcon : ArrowLeftIcon}
|
|
||||||
title={intl.formatMessage(
|
|
||||||
expanded ? messages.collapse : messages.expand,
|
|
||||||
)}
|
|
||||||
onClick={handleClick}
|
|
||||||
aria-controls={`${accessibilityId}-content`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{lists.length > 0 && expanded && (
|
|
||||||
<div
|
|
||||||
className='navigation-panel__list-panel__items'
|
|
||||||
role='region'
|
|
||||||
id={`${accessibilityId}-content`}
|
|
||||||
aria-labelledby={`${accessibilityId}-title`}
|
|
||||||
>
|
|
||||||
{lists.map((list) => (
|
|
||||||
<ColumnLink
|
|
||||||
icon='list-ul'
|
|
||||||
key={list.get('id')}
|
|
||||||
iconComponent={ListAltIcon}
|
|
||||||
activeIconComponent={ListAltActiveIcon}
|
|
||||||
text={list.get('title')}
|
|
||||||
to={`/lists/${list.get('id')}`}
|
|
||||||
transparent
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { registrationsOpen, sso_redirect } from 'mastodon/initial_state';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
|
||||||
|
|
||||||
const SignInBanner = () => {
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const openClosedRegistrationsModal = useCallback(
|
|
||||||
() => dispatch(openModal({ modalType: 'CLOSED_REGISTRATIONS' })),
|
|
||||||
[dispatch],
|
|
||||||
);
|
|
||||||
|
|
||||||
let signupButton;
|
|
||||||
|
|
||||||
const signupUrl = useAppSelector((state) => state.getIn(['server', 'server', 'registrations', 'url'], null) || '/auth/sign_up');
|
|
||||||
|
|
||||||
if (sso_redirect) {
|
|
||||||
return (
|
|
||||||
<div className='sign-in-banner'>
|
|
||||||
<p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p>
|
|
||||||
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
|
|
||||||
<a href={sso_redirect} data-method='post' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sso_redirect' defaultMessage='Login or Register' /></a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registrationsOpen) {
|
|
||||||
signupButton = (
|
|
||||||
<a href={signupUrl} className='button button--block'>
|
|
||||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
signupButton = (
|
|
||||||
<button className='button button--block' onClick={openClosedRegistrationsModal}>
|
|
||||||
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='sign-in-banner'>
|
|
||||||
<p><strong><FormattedMessage id='sign_in_banner.mastodon_is' defaultMessage="Mastodon is the best way to keep up with what's happening." /></strong></p>
|
|
||||||
<p><FormattedMessage id='sign_in_banner.follow_anyone' defaultMessage='Follow anyone across the fediverse and see it all in chronological order. No algorithms, ads, or clickbait in sight.' /></p>
|
|
||||||
{signupButton}
|
|
||||||
<a href='/auth/sign_in' className='button button--block button-tertiary'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Login' /></a>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SignInBanner;
|
|
|
@ -579,8 +579,11 @@
|
||||||
"navigation_bar.privacy_and_reach": "Privacy and reach",
|
"navigation_bar.privacy_and_reach": "Privacy and reach",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.search": "Search",
|
"navigation_bar.search": "Search",
|
||||||
|
"navigation_bar.search_trends": "Search / Trending",
|
||||||
"navigation_bar.security": "Security",
|
"navigation_bar.security": "Security",
|
||||||
|
"navigation_panel.collapse_followed_tags": "Collapse followed hashtags menu",
|
||||||
"navigation_panel.collapse_lists": "Collapse list menu",
|
"navigation_panel.collapse_lists": "Collapse list menu",
|
||||||
|
"navigation_panel.expand_followed_tags": "Expand followed hashtags menu",
|
||||||
"navigation_panel.expand_lists": "Expand list menu",
|
"navigation_panel.expand_lists": "Expand list menu",
|
||||||
"not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
|
"not_signed_in_indicator.not_signed_in": "You need to login to access this resource.",
|
||||||
"notification.admin.report": "{name} reported {target}",
|
"notification.admin.report": "{name} reported {target}",
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import type { Reducer } from '@reduxjs/toolkit';
|
import type { Reducer } from '@reduxjs/toolkit';
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
import { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
import { createList, updateList } from 'mastodon/actions/lists_typed';
|
import {
|
||||||
|
createList,
|
||||||
|
updateList,
|
||||||
|
fetchLists,
|
||||||
|
} from 'mastodon/actions/lists_typed';
|
||||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||||
import { createList as createListFromJSON } from 'mastodon/models/list';
|
import { createList as createListFromJSON } from 'mastodon/models/list';
|
||||||
import type { List } from 'mastodon/models/list';
|
import type { List } from 'mastodon/models/list';
|
||||||
|
@ -9,7 +13,6 @@ import type { List } from 'mastodon/models/list';
|
||||||
import {
|
import {
|
||||||
LIST_FETCH_SUCCESS,
|
LIST_FETCH_SUCCESS,
|
||||||
LIST_FETCH_FAIL,
|
LIST_FETCH_FAIL,
|
||||||
LISTS_FETCH_SUCCESS,
|
|
||||||
LIST_DELETE_SUCCESS,
|
LIST_DELETE_SUCCESS,
|
||||||
} from '../actions/lists';
|
} from '../actions/lists';
|
||||||
|
|
||||||
|
@ -33,12 +36,12 @@ export const listsReducer: Reducer<State> = (state = initialState, action) => {
|
||||||
updateList.fulfilled.match(action)
|
updateList.fulfilled.match(action)
|
||||||
) {
|
) {
|
||||||
return normalizeList(state, action.payload);
|
return normalizeList(state, action.payload);
|
||||||
|
} else if (fetchLists.fulfilled.match(action)) {
|
||||||
|
return normalizeLists(state, action.payload);
|
||||||
} else {
|
} else {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case LIST_FETCH_SUCCESS:
|
case LIST_FETCH_SUCCESS:
|
||||||
return normalizeList(state, action.list as ApiListJSON);
|
return normalizeList(state, action.list as ApiListJSON);
|
||||||
case LISTS_FETCH_SUCCESS:
|
|
||||||
return normalizeLists(state, action.lists as ApiListJSON[]);
|
|
||||||
case LIST_DELETE_SUCCESS:
|
case LIST_DELETE_SUCCESS:
|
||||||
case LIST_FETCH_FAIL:
|
case LIST_FETCH_FAIL:
|
||||||
return state.set(action.id as string, null);
|
return state.set(action.id as string, null);
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/></svg>
|
After Width: | Height: | Size: 159 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-344 240-584l56-56 184 184 184-184 56 56-240 240Z"/></svg>
|
After Width: | Height: | Size: 159 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z"/></svg>
|
After Width: | Height: | Size: 160 B |
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z"/></svg>
|
After Width: | Height: | Size: 160 B |
|
@ -3144,10 +3144,9 @@ a.account__display-name {
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-panel__sign-in-banner,
|
|
||||||
.navigation-panel__banner,
|
.navigation-panel__banner,
|
||||||
.getting-started__trends,
|
.navigation-panel__logo,
|
||||||
.navigation-panel__logo {
|
.getting-started__trends {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3481,15 +3480,24 @@ a.account__display-name {
|
||||||
&__header {
|
&__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-inline-end: 12px;
|
|
||||||
|
&__sep {
|
||||||
|
width: 0;
|
||||||
|
height: 24px;
|
||||||
|
border-left: 1px solid var(--background-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
.column-link {
|
.column-link {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__items {
|
&__items {
|
||||||
padding-inline-start: 24px + 5px;
|
padding-inline-start: 24px + 8px;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -3500,14 +3508,16 @@ a.account__display-name {
|
||||||
&__compose-button {
|
&__compose-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
padding-top: 10px;
|
padding-top: 8px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 8px;
|
||||||
padding-inline-start: 13px - 7px;
|
padding-inline-start: 12px - 7px;
|
||||||
padding-inline-end: 13px;
|
padding-inline-end: 12px;
|
||||||
gap: 5px;
|
gap: 8px;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
margin-bottom: 4px;
|
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
|
@ -3517,7 +3527,11 @@ a.account__display-name {
|
||||||
|
|
||||||
.navigation-bar {
|
.navigation-bar {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
@ -3529,26 +3543,36 @@ a.account__display-name {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (height <= 710px) {
|
.getting-started__trends h4 {
|
||||||
&__portal {
|
padding: 10px 12px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.getting-started__trends .trends__item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (height <= 930px) {
|
||||||
|
&__portal .trends__item:nth-child(n + 5) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (height <= 765px) {
|
@media screen and (height <= 930px - 56px) {
|
||||||
&__portal .trends__item:nth-child(n + 3) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (height <= 820px) {
|
|
||||||
&__portal .trends__item:nth-child(n + 4) {
|
&__portal .trends__item:nth-child(n + 4) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (height <= 920px) {
|
@media screen and (height <= 930px - (56px * 2)) {
|
||||||
.column-link.column-link--optional {
|
&__portal .trends__item:nth-child(n + 3) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (height <= 930px - (56px * 3)) {
|
||||||
|
&__portal {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3821,9 +3845,10 @@ a.account__display-name {
|
||||||
.column-link {
|
.column-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 8px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
padding: 13px;
|
font-weight: 400;
|
||||||
|
padding: 12px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -4605,7 +4630,8 @@ a.status-card {
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-more .loading-indicator,
|
.load-more .loading-indicator,
|
||||||
.button .loading-indicator {
|
.button .loading-indicator,
|
||||||
|
.icon-button .loading-indicator {
|
||||||
position: static;
|
position: static;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|
||||||
|
@ -4616,6 +4642,13 @@ a.status-card {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-button .loading-indicator .circular-progress {
|
||||||
|
color: lighten($ui-base-color, 26%);
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
margin: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.load-more .loading-indicator .circular-progress {
|
.load-more .loading-indicator .circular-progress {
|
||||||
color: lighten($ui-base-color, 26%);
|
color: lighten($ui-base-color, 26%);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user