mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-12 16:35:57 -05:00
Redesign: Add new column header to "Saved Posts" and "Post Likes" pages (#40345)
This commit is contained in:
@@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
import ArrowBackIcon from '@/material-icons/400-24px/arrow_back.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { getColumnSkipLinkId } from 'mastodon/features/ui/components/skip_links';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { RenderIntoTabsBarPortal } from 'mastodon/features/ui/util/columns_context';
|
||||
|
||||
import { useAppHistory } from '../router';
|
||||
|
||||
@@ -49,5 +49,5 @@ export const ColumnBackButton: React.FC<{ onClick?: OnClickCallback }> = ({
|
||||
</button>
|
||||
);
|
||||
|
||||
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||
return <RenderIntoTabsBarPortal>{component}</RenderIntoTabsBarPortal>;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import UnfoldLessIcon from '@/material-icons/400-24px/unfold_less.svg?react';
|
||||
import UnfoldMoreIcon from '@/material-icons/400-24px/unfold_more.svg?react';
|
||||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { ButtonInTabsBar } from 'mastodon/features/ui/util/columns_context';
|
||||
import { RenderIntoTabsBarPortal } from 'mastodon/features/ui/util/columns_context';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
|
||||
import { getColumnSkipLinkId } from '../../features/ui/components/skip_links';
|
||||
@@ -340,6 +340,6 @@ export const ColumnHeader: React.FC<ColumnHeaderProps> = ({
|
||||
if (placeholder) {
|
||||
return component;
|
||||
} else {
|
||||
return <ButtonInTabsBar>{component}</ButtonInTabsBar>;
|
||||
return <RenderIntoTabsBarPortal>{component}</RenderIntoTabsBarPortal>;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ArrowLeftIcon, ListIcon } from '@phosphor-icons/react';
|
||||
import { openNavigation } from '@/mastodon/actions/navigation';
|
||||
import { getColumnSkipLinkId } from '@/mastodon/features/ui/components/skip_links';
|
||||
import { useBreakpoint } from '@/mastodon/features/ui/hooks/useBreakpoint';
|
||||
import { RenderIntoTabsBarPortal } from '@/mastodon/features/ui/util/columns_context';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import { hasReactChildren } from '@/mastodon/utils/has_react_children';
|
||||
|
||||
@@ -38,21 +39,23 @@ export const ColumnHeader: React.FC<ColumnHeaderProps> = ({
|
||||
const columnIndex = useColumnIndexContext();
|
||||
|
||||
return (
|
||||
<header {...props} className={classNames(className, classes.root)}>
|
||||
{withBackButton ? <BackButton /> : <MobileMenuButton />}
|
||||
<NavigationFocusTarget className={classes.title}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={scrollTop}
|
||||
id={getColumnSkipLinkId(columnIndex)}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</NavigationFocusTarget>
|
||||
{hasReactChildren(extraButtons) && (
|
||||
<div className={classes.rightButtons}>{extraButtons}</div>
|
||||
)}
|
||||
</header>
|
||||
<RenderIntoTabsBarPortal>
|
||||
<header {...props} className={classNames(className, classes.root)}>
|
||||
{withBackButton ? <BackButton /> : <MobileMenuButton />}
|
||||
<NavigationFocusTarget className={classes.title}>
|
||||
<button
|
||||
type='button'
|
||||
onClick={scrollTop}
|
||||
id={getColumnSkipLinkId(columnIndex)}
|
||||
>
|
||||
{title}
|
||||
</button>
|
||||
</NavigationFocusTarget>
|
||||
{hasReactChildren(extraButtons) && (
|
||||
<div className={classes.rightButtons}>{extraButtons}</div>
|
||||
)}
|
||||
</header>
|
||||
</RenderIntoTabsBarPortal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
color: var(--color-text-primary);
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--color-bg-primary) 50%,
|
||||
var(--color-bg-primary) var(--column-header-gradient-start, 55%),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import { Column } from '@/mastodon/components/column';
|
||||
import { ColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { ColumnHeader } from '@/mastodon/components/column_header';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import BookmarksIcon from '@/material-icons/400-24px/bookmarks-fill.svg?react';
|
||||
import {
|
||||
fetchBookmarkedStatuses,
|
||||
@@ -18,6 +20,7 @@ import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
headingRedesign: { id: 'column.saved_posts', defaultMessage: 'Saved Posts' },
|
||||
});
|
||||
|
||||
const Bookmarks: React.FC<{
|
||||
@@ -74,16 +77,20 @@ const Bookmarks: React.FC<{
|
||||
bindToDocument={!multiColumn}
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
>
|
||||
<ColumnHeader
|
||||
icon='bookmarks'
|
||||
iconComponent={BookmarksIcon}
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onPin={handlePin}
|
||||
onMove={handleMove}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
scrollTopOnClick
|
||||
/>
|
||||
{isRedesignEnabled() ? (
|
||||
<ColumnHeader title={intl.formatMessage(messages.headingRedesign)} />
|
||||
) : (
|
||||
<LegacyColumnHeader
|
||||
icon='bookmarks'
|
||||
iconComponent={BookmarksIcon}
|
||||
title={intl.formatMessage(messages.heading)}
|
||||
onPin={handlePin}
|
||||
onMove={handleMove}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
scrollTopOnClick
|
||||
/>
|
||||
)}
|
||||
|
||||
<StatusList
|
||||
trackScroll={!pinned}
|
||||
@@ -98,7 +105,11 @@ const Bookmarks: React.FC<{
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.heading)}</title>
|
||||
<title>
|
||||
{isRedesignEnabled()
|
||||
? intl.formatMessage(messages.headingRedesign)
|
||||
: intl.formatMessage(messages.heading)}
|
||||
</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||
import { fetchFavourites, expandFavourites } from 'mastodon/actions/interactions';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
accountIds: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'items']),
|
||||
hasMore: !!state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'next']),
|
||||
isLoading: state.getIn(['user_lists', 'favourited_by', props.params.statusId, 'isLoading'], true),
|
||||
});
|
||||
|
||||
class Favourites extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
params: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
accountIds: ImmutablePropTypes.list,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
if (!this.props.accountIds) {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
}
|
||||
}
|
||||
|
||||
handleRefresh = () => {
|
||||
this.props.dispatch(fetchFavourites(this.props.params.statusId));
|
||||
};
|
||||
|
||||
handleLoadMore = debounce(() => {
|
||||
this.props.dispatch(expandFavourites(this.props.params.statusId));
|
||||
}, 300, { leading: true });
|
||||
|
||||
render () {
|
||||
const { intl, accountIds, hasMore, isLoading, multiColumn } = this.props;
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.favourites' defaultMessage='No one has favorited this post yet. When someone does, they will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<ColumnHeader
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
extraButton={(
|
||||
<button type='button' className='column-header__button' title={intl.formatMessage(messages.refresh)} aria-label={intl.formatMessage(messages.refresh)} onClick={this.handleRefresh}><Icon id='refresh' icon={RefreshIcon} /></button>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='favourites'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{accountIds.map(id =>
|
||||
<Account key={id} id={id} reference='status' />,
|
||||
)}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Favourites));
|
||||
142
app/javascript/mastodon/features/favourites/index.tsx
Normal file
142
app/javascript/mastodon/features/favourites/index.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { ArrowClockwiseIcon } from '@phosphor-icons/react';
|
||||
import { Helmet } from '@unhead/react/helmet';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header';
|
||||
import {
|
||||
ColumnHeader,
|
||||
ColumnHeaderButton,
|
||||
} from '@/mastodon/components/column_header';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import RefreshIcon from '@/material-icons/400-24px/refresh.svg?react';
|
||||
import {
|
||||
fetchFavourites,
|
||||
expandFavourites,
|
||||
} from 'mastodon/actions/interactions';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'status.likes_title', defaultMessage: 'Post Likes' },
|
||||
refresh: { id: 'refresh', defaultMessage: 'Refresh' },
|
||||
});
|
||||
|
||||
const Favourites: React.FC<{ multiColumn?: boolean }> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const { statusId } = useParams<{ statusId?: string }>();
|
||||
|
||||
const { accountIds, hasMore, isLoading } = useAppSelector((state) => ({
|
||||
accountIds: state.user_lists.getIn(['favourited_by', statusId, 'items']) as
|
||||
| ImmutableList<string>
|
||||
| undefined,
|
||||
hasMore: !!state.user_lists.getIn(['favourited_by', statusId, 'next']),
|
||||
isLoading: state.user_lists.getIn(
|
||||
['favourited_by', statusId, 'isLoading'],
|
||||
true,
|
||||
) as boolean,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountIds) {
|
||||
dispatch(fetchFavourites(statusId));
|
||||
}
|
||||
}, [accountIds, dispatch, statusId]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
dispatch(fetchFavourites(statusId));
|
||||
}, [dispatch, statusId]);
|
||||
|
||||
const handleLoadMore = useDebouncedCallback(
|
||||
() => {
|
||||
dispatch(expandFavourites(statusId));
|
||||
},
|
||||
300,
|
||||
{
|
||||
leading: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (!accountIds) {
|
||||
return (
|
||||
<Column>
|
||||
<LoadingIndicator />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = (
|
||||
<FormattedMessage
|
||||
id='empty_column.favourites'
|
||||
defaultMessage='No one has favorited this post yet. When someone does, they will show up here.'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
{isRedesignEnabled() ? (
|
||||
<ColumnHeader
|
||||
withBackButton
|
||||
title={intl.formatMessage(messages.title)}
|
||||
extraButtons={
|
||||
<ColumnHeaderButton
|
||||
icon={ArrowClockwiseIcon}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{intl.formatMessage(messages.refresh)}
|
||||
</ColumnHeaderButton>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LegacyColumnHeader
|
||||
showBackButton
|
||||
multiColumn={multiColumn}
|
||||
extraButton={
|
||||
<button
|
||||
type='button'
|
||||
className='column-header__button'
|
||||
title={intl.formatMessage(messages.refresh)}
|
||||
aria-label={intl.formatMessage(messages.refresh)}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
<Icon id='refresh' icon={RefreshIcon} />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='favourites'
|
||||
onLoadMore={handleLoadMore}
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
>
|
||||
{accountIds.map((id) => (
|
||||
<Account key={id} id={id} reference='status' />
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<Helmet>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default Favourites;
|
||||
@@ -51,20 +51,24 @@
|
||||
container: column / inline-size;
|
||||
color: var(--color-text-primary);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
@media ($mobile-menu-breakpoint < width < $no-gap-breakpoint) {
|
||||
border-inline: 1px solid var(--color-border-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.columnHeader {
|
||||
background: var(--color-bg-blend-solid);
|
||||
// Make bottom half of column header transparent to make the
|
||||
// overlay gradient of the nested `ColumnHeader` component work
|
||||
--column-header-gradient-start: 55%;
|
||||
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--color-bg-blend-solid) var(--column-header-gradient-start),
|
||||
transparent var(--column-header-gradient-start)
|
||||
);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
border-top: 0;
|
||||
|
||||
@media (width >= $no-gap-breakpoint) {
|
||||
@media (width >= $mobile-menu-breakpoint) {
|
||||
border-top: 10px solid var(--color-bg-blend-solid);
|
||||
}
|
||||
|
||||
@@ -74,3 +78,22 @@
|
||||
background: var(--color-bg-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
:global(.column) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.scrollable) {
|
||||
contain: content;
|
||||
overflow: visible;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useColumnsContext } from '../../util/columns_context';
|
||||
import { MultiColumnContent } from './multi_column_content';
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
const TabsBarPortal = () => {
|
||||
const TabsBarPortal: React.FC<React.ComponentProps<'div'>> = (props) => {
|
||||
const { setTabsBarElement } = useColumnsContext();
|
||||
|
||||
const setRef = useCallback(
|
||||
@@ -27,7 +27,7 @@ const TabsBarPortal = () => {
|
||||
[setTabsBarElement],
|
||||
);
|
||||
|
||||
return <div id='tabs-bar__portal' ref={setRef} />;
|
||||
return <div {...props} ref={setRef} />;
|
||||
};
|
||||
|
||||
export const ColumnsAreaRedesign: React.FC<{
|
||||
@@ -47,11 +47,9 @@ export const ColumnsAreaRedesign: React.FC<{
|
||||
<div className={classes.main}>
|
||||
<Header />
|
||||
|
||||
<div className='tabs-bar__wrapper'>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
<TabsBarPortal className={classes.columnHeader} />
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
<div className={classes.content}>{children}</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -68,11 +66,9 @@ export const ColumnsAreaRedesign: React.FC<{
|
||||
{isMobile ? <RedesignMobileNavigation /> : <ComposeRedesignButton />}
|
||||
|
||||
<main className={classes.main}>
|
||||
<div className={classes.columnHeader}>
|
||||
<TabsBarPortal />
|
||||
</div>
|
||||
<TabsBarPortal className={classes.columnHeader} />
|
||||
|
||||
<div className='columns-area columns-area--mobile'>{children}</div>
|
||||
<div className={classes.content}>{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ export function useColumnsContext() {
|
||||
return useContext(ColumnsContext);
|
||||
}
|
||||
|
||||
export const ButtonInTabsBar: React.FC<{
|
||||
export const RenderIntoTabsBarPortal: React.FC<{
|
||||
children: ReactElement | string | undefined;
|
||||
}> = ({ children }) => {
|
||||
const { multiColumn, tabsBarElement } = useColumnsContext();
|
||||
|
||||
@@ -470,6 +470,7 @@
|
||||
"column.other_collections": "Collections by {name}",
|
||||
"column.pins": "Pinned posts",
|
||||
"column.public": "Federated timeline",
|
||||
"column.saved_posts": "Saved Posts",
|
||||
"column.your_collections": "Your Collections",
|
||||
"column_back_button.label": "Back",
|
||||
"column_header.hide_settings": "Hide settings",
|
||||
@@ -1335,6 +1336,7 @@
|
||||
"status.filter": "Filter this post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
"status.likes_title": "Post Likes",
|
||||
"status.load_more": "Load more",
|
||||
"status.media.open": "Click to open",
|
||||
"status.media.show": "Click to show",
|
||||
|
||||
Reference in New Issue
Block a user