mirror of
https://github.com/mastodon/mastodon.git
synced 2026-09-13 01:15:48 -05:00
Redesign: Update followed hashtag column header (#40384)
This commit is contained in:
@@ -12,6 +12,7 @@ import { Toggle } from '@/mastodon/components/form_fields/toggle_field';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
|
||||
import SettingToggle from '../../notifications/components/setting_toggle';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
|
||||
@@ -108,6 +109,16 @@ class ColumnSettings extends PureComponent {
|
||||
render () {
|
||||
const { settings, onChange } = this.props;
|
||||
|
||||
if (isRedesignEnabled()) {
|
||||
return (
|
||||
<div className='column-settings column-settings__hashtags'>
|
||||
{this.modeSelect('any')}
|
||||
{this.modeSelect('all')}
|
||||
{this.modeSelect('none')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
@@ -115,7 +126,7 @@ class ColumnSettings extends PureComponent {
|
||||
<SettingToggle settings={settings} settingPath={['local']} onChange={onChange} label={<FormattedMessage id='community.column_settings.local_only' defaultMessage='Local only' />} />
|
||||
|
||||
<div className='setting-toggle'>
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
|
||||
<Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} size={16} />
|
||||
|
||||
<span className='setting-toggle__label'>
|
||||
<FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { Button } from '@/mastodon/components/button/redesign';
|
||||
import {
|
||||
ModalActions,
|
||||
ModalShell,
|
||||
ModalTitle,
|
||||
} from '@/mastodon/components/modal_shell/redesign';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import ColumnSettingsContainer from '../containers/column_settings_container';
|
||||
|
||||
const HashtagSettingsModal: React.FC<{ columnId: string; tagId: string }> = ({
|
||||
columnId,
|
||||
tagId,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleCloseModal = useCallback(() => {
|
||||
void dispatch(
|
||||
closeModal({ modalType: 'HASHTAG_SETTINGS', ignoreFocus: false }),
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<ModalShell>
|
||||
<ModalTitle onClose={handleCloseModal}>
|
||||
<FormattedMessage
|
||||
id='hashtags.add_more_tags_to_column'
|
||||
defaultMessage='Add more tags to #{tag}'
|
||||
values={{ tag: tagId }}
|
||||
/>
|
||||
</ModalTitle>
|
||||
<ColumnSettingsContainer columnId={columnId} />
|
||||
<ModalActions>
|
||||
<Button variant='solid' onClick={handleCloseModal}>
|
||||
<FormattedMessage id='alt_text_modal.done' defaultMessage='Done' />
|
||||
</Button>
|
||||
</ModalActions>
|
||||
</ModalShell>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default HashtagSettingsModal;
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { changeColumnParams } from '@/mastodon/actions/columns';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { ColumnSettingsMenu } from '@/mastodon/components/column_header';
|
||||
import { MultiColumnMenuItems } from '@/mastodon/components/column_header/multicolumn_settings';
|
||||
import {
|
||||
MenuItem,
|
||||
MenuItemCheckbox,
|
||||
MenuItemDivider,
|
||||
} from '@/mastodon/components/menu';
|
||||
import type { MenuItemCheckboxChangeHandler } from '@/mastodon/components/menu/items';
|
||||
import { useIdentity } from '@/mastodon/identity_context';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { useColumnSettings } from '../../public_timeline/components/feed_column_settings';
|
||||
|
||||
import { useHashtag, messages } from './hashtag_header';
|
||||
|
||||
export const HashtagColumnMenu: React.FC<{
|
||||
tagId: string;
|
||||
multiColumn?: boolean;
|
||||
columnId?: string;
|
||||
onPin: () => void;
|
||||
onMove: (dir: number) => void;
|
||||
}> = ({ tagId, multiColumn, columnId, onPin, onMove }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { signedIn } = useIdentity();
|
||||
|
||||
const { tag, toggleFollow, toggleFeature } = useHashtag(tagId);
|
||||
|
||||
const columnSettings = useColumnSettings(columnId);
|
||||
const isLocalOnly = columnSettings.get('local') as boolean;
|
||||
const toggleIsLocalOnly = useCallback<MenuItemCheckboxChangeHandler>(
|
||||
({ checked }) => {
|
||||
dispatch(changeColumnParams(columnId, ['local'], checked));
|
||||
},
|
||||
[columnId, dispatch],
|
||||
);
|
||||
|
||||
const openAdvancedSettings = useCallback(() => {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'HASHTAG_SETTINGS',
|
||||
modalProps: { columnId, tagId },
|
||||
}),
|
||||
);
|
||||
}, [columnId, dispatch, tagId]);
|
||||
|
||||
if (!tag || !signedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pinned = !!columnId;
|
||||
|
||||
return (
|
||||
<ColumnSettingsMenu
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='hashtag.options'
|
||||
defaultMessage='Hashtag options'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<MenuItem onClick={toggleFollow}>
|
||||
{tag.following ? (
|
||||
<FormattedMessage {...messages.unfollowHashtag} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.followHashtag} />
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleFeature}>
|
||||
{tag.featuring ? (
|
||||
<FormattedMessage {...messages.unfeature} />
|
||||
) : (
|
||||
<FormattedMessage {...messages.feature} />
|
||||
)}
|
||||
</MenuItem>
|
||||
{multiColumn && pinned && (
|
||||
<>
|
||||
<MenuItemDivider />
|
||||
<MenuItemCheckbox
|
||||
value='local'
|
||||
checked={isLocalOnly}
|
||||
onChange={toggleIsLocalOnly}
|
||||
keepMenuOpenOnClick
|
||||
>
|
||||
<FormattedMessage
|
||||
id='community.column_settings.local_only'
|
||||
defaultMessage='Local only'
|
||||
/>
|
||||
</MenuItemCheckbox>
|
||||
<MenuItem onClick={openAdvancedSettings}>
|
||||
<FormattedMessage
|
||||
id='hashtags.add_more_tags'
|
||||
defaultMessage='Add more tags to this column…'
|
||||
values={{ tag: tagId }}
|
||||
/>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{multiColumn && (
|
||||
<MultiColumnMenuItems
|
||||
withDivider
|
||||
pinned={pinned}
|
||||
onPin={onPin}
|
||||
onMove={onMove}
|
||||
/>
|
||||
)}
|
||||
</ColumnSettingsMenu>
|
||||
);
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import { useIdentity } from 'mastodon/identity_context';
|
||||
import { PERMISSION_MANAGE_TAXONOMIES } from 'mastodon/permissions';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
export const messages = defineMessages({
|
||||
followHashtag: { id: 'hashtag.follow', defaultMessage: 'Follow hashtag' },
|
||||
unfollowHashtag: {
|
||||
id: 'hashtag.unfollow',
|
||||
@@ -76,11 +76,7 @@ const usesTodayRenderer = (
|
||||
/>
|
||||
);
|
||||
|
||||
export const HashtagHeader: React.FC<{
|
||||
tagId: string;
|
||||
}> = ({ tagId }) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn, permissions } = useIdentity();
|
||||
export function useHashtag(tagId: string) {
|
||||
const dispatch = useAppDispatch();
|
||||
const [tag, setTag] = useState<ApiHashtagJSON>();
|
||||
|
||||
@@ -94,54 +90,32 @@ export const HashtagHeader: React.FC<{
|
||||
});
|
||||
}, [dispatch, tagId, setTag]);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const arr = [];
|
||||
|
||||
if (tag && signedIn) {
|
||||
const handleFeature = () => {
|
||||
if (tag.featuring) {
|
||||
void dispatch(unfeatureHashtag({ tagId })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
setTag(result.payload);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(featureHashtag({ tagId })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
setTag(result.payload);
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
arr.push({
|
||||
text: intl.formatMessage(
|
||||
tag.featuring ? messages.unfeature : messages.feature,
|
||||
),
|
||||
action: handleFeature,
|
||||
});
|
||||
|
||||
arr.push(null);
|
||||
|
||||
if (
|
||||
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
|
||||
PERMISSION_MANAGE_TAXONOMIES
|
||||
) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(messages.adminModeration, { name: tagId }),
|
||||
href: `/admin/tags/${tag.id}`,
|
||||
});
|
||||
}
|
||||
const toggleFeature = useCallback(() => {
|
||||
if (!tag) {
|
||||
return;
|
||||
}
|
||||
if (tag.featuring) {
|
||||
void dispatch(unfeatureHashtag({ tagId })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
setTag(result.payload);
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [setTag, dispatch, tagId, signedIn, permissions, intl, tag]);
|
||||
return '';
|
||||
});
|
||||
} else {
|
||||
void dispatch(featureHashtag({ tagId })).then((result) => {
|
||||
if (isFulfilled(result)) {
|
||||
setTag(result.payload);
|
||||
}
|
||||
|
||||
const handleFollow = useCallback(() => {
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [dispatch, tag, tagId]);
|
||||
|
||||
const { signedIn } = useIdentity();
|
||||
|
||||
const toggleFollow = useCallback(() => {
|
||||
if (!signedIn || !tag) {
|
||||
return;
|
||||
}
|
||||
@@ -167,7 +141,44 @@ export const HashtagHeader: React.FC<{
|
||||
return '';
|
||||
});
|
||||
}
|
||||
}, [dispatch, setTag, signedIn, tag, tagId]);
|
||||
}, [dispatch, signedIn, tag, tagId]);
|
||||
|
||||
return { tag, toggleFollow, toggleFeature };
|
||||
}
|
||||
|
||||
export const HashtagHeader: React.FC<{
|
||||
tagId: string;
|
||||
}> = ({ tagId }) => {
|
||||
const intl = useIntl();
|
||||
const { signedIn, permissions } = useIdentity();
|
||||
const { tag, toggleFeature, toggleFollow } = useHashtag(tagId);
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const arr = [];
|
||||
|
||||
if (tag && signedIn) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(
|
||||
tag.featuring ? messages.unfeature : messages.feature,
|
||||
),
|
||||
action: toggleFeature,
|
||||
});
|
||||
|
||||
arr.push(null);
|
||||
|
||||
if (
|
||||
(permissions & PERMISSION_MANAGE_TAXONOMIES) ===
|
||||
PERMISSION_MANAGE_TAXONOMIES
|
||||
) {
|
||||
arr.push({
|
||||
text: intl.formatMessage(messages.adminModeration, { name: tagId }),
|
||||
href: `/admin/tags/${tag.id}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [tag, signedIn, intl, toggleFeature, permissions, tagId]);
|
||||
|
||||
if (!tag) {
|
||||
return null;
|
||||
@@ -199,7 +210,7 @@ export const HashtagHeader: React.FC<{
|
||||
|
||||
{signedIn && (
|
||||
<Button
|
||||
onClick={handleFollow}
|
||||
onClick={toggleFollow}
|
||||
text={intl.formatMessage(
|
||||
tag.following
|
||||
? messages.unfollowHashtag
|
||||
|
||||
@@ -14,7 +14,7 @@ import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
import { connectHashtagStream } from 'mastodon/actions/streaming';
|
||||
import { expandHashtagTimeline, clearTimeline } from 'mastodon/actions/timelines';
|
||||
import { Column } from '@/mastodon/components/column';
|
||||
import { ColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { ColumnHeader as LegacyColumnHeader } from '@/mastodon/components/column/header';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
import { remoteTopicFeedAccess, me, localTopicFeedAccess } from 'mastodon/initial_state';
|
||||
|
||||
@@ -22,6 +22,9 @@ import StatusListContainer from '../ui/containers/status_list_container';
|
||||
|
||||
import { HashtagHeader } from './components/hashtag_header';
|
||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||
import { ColumnHeader } from '@/mastodon/components/column_header';
|
||||
import { isRedesignEnabled } from '@/mastodon/utils/environment';
|
||||
import { HashtagColumnMenu } from './components/hashtag_column_menu';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const local = props.params.local || (!me && remoteTopicFeedAccess !== 'public');
|
||||
@@ -163,26 +166,44 @@ class HashtagTimeline extends PureComponent {
|
||||
const { hasUnread, columnId, multiColumn, local, hasFeedAccess } = this.props;
|
||||
const { id } = this.props.params;
|
||||
const pinned = !!columnId;
|
||||
const withHeadingSection = !isRedesignEnabled() && !pinned;
|
||||
|
||||
const title = <>#{this.title()}</>;
|
||||
const titleAsString = `#${id}`;
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} label={`#${id}`}>
|
||||
<ColumnHeader
|
||||
icon='hashtag'
|
||||
iconComponent={TagIcon}
|
||||
active={hasUnread}
|
||||
title={this.title()}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
scrollTopOnClick
|
||||
>
|
||||
{columnId && <ColumnSettingsContainer columnId={columnId} />}
|
||||
</ColumnHeader>
|
||||
<Column bindToDocument={!multiColumn} label={titleAsString}>
|
||||
{isRedesignEnabled() ? (
|
||||
<ColumnHeader
|
||||
title={title}
|
||||
withUnreadMarker={hasUnread}
|
||||
withBackButton={multiColumn && !pinned && 'auto'}
|
||||
extraButtons={
|
||||
<HashtagColumnMenu
|
||||
tagId={id}
|
||||
multiColumn={multiColumn}
|
||||
columnId={columnId}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LegacyColumnHeader
|
||||
icon='hashtag'
|
||||
iconComponent={TagIcon}
|
||||
active={hasUnread}
|
||||
title={this.title()}
|
||||
multiColumn={multiColumn}
|
||||
showBackButton
|
||||
scrollTopOnClick
|
||||
>
|
||||
{columnId && <ColumnSettingsContainer columnId={columnId} />}
|
||||
</LegacyColumnHeader>
|
||||
)}
|
||||
|
||||
<StatusListContainer
|
||||
prepend={pinned ? null : <HashtagHeader tagId={id} />}
|
||||
prepend={withHeadingSection && <HashtagHeader tagId={id} />}
|
||||
alwaysPrepend
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`hashtag_timeline-${columnId}`}
|
||||
@@ -206,7 +227,7 @@ class HashtagTimeline extends PureComponent {
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
<title>#{id}</title>
|
||||
<title>{titleAsString}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { SlidersHorizontalIcon } from '@phosphor-icons/react';
|
||||
|
||||
import { ColumnSettingsMenu } from '@/mastodon/components/column_header';
|
||||
@@ -16,7 +18,9 @@ export const HomeColumnSettings: React.FC<{
|
||||
}> = ({ children }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useAppSelector((state) => state.settings.get('home'));
|
||||
const settings = useAppSelector((state) =>
|
||||
state.settings.get('home'),
|
||||
) as ImmutableMap<string, unknown>;
|
||||
const onChange = useCallback<MenuItemCheckboxChangeHandler>(
|
||||
({ value, checked }) => {
|
||||
dispatch(changeSetting(['home', 'shows', value], checked));
|
||||
@@ -24,14 +28,9 @@ export const HomeColumnSettings: React.FC<{
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
// @ts-expect-error settings isn't typed yet
|
||||
const showBoosts = settings.getIn(['shows', 'reblog']) as boolean;
|
||||
// @ts-expect-error settings isn't typed yet
|
||||
const showQuotes = settings.getIn(['shows', 'quote']) as boolean;
|
||||
// @ts-expect-error settings isn't typed yet
|
||||
const showReplies = settings.getIn(['shows', 'reply']) as boolean;
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
return (
|
||||
<ColumnSettingsMenu
|
||||
|
||||
@@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import type {
|
||||
List as ImmutableList,
|
||||
Map as ImmutableMap,
|
||||
Record as ImmutableRecord,
|
||||
} from 'immutable';
|
||||
|
||||
@@ -21,20 +22,24 @@ type Columns = ImmutableList<
|
||||
}>
|
||||
>;
|
||||
|
||||
export const FeedColumnSettings: React.FC<{
|
||||
columnId: string | undefined;
|
||||
localOnly?: boolean;
|
||||
}> = ({ columnId, localOnly }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useAppSelector((state) => {
|
||||
export function useColumnSettings(columnId: string | undefined) {
|
||||
return useAppSelector((state) => {
|
||||
const columns = state.settings.get('columns') as Columns;
|
||||
const index = columns.findIndex((c) => c.get('uuid') === columnId);
|
||||
|
||||
return columnId && index >= 0
|
||||
? columns.get(index)?.get('params')
|
||||
: state.settings.get('public');
|
||||
});
|
||||
}) as ImmutableMap<string, unknown>;
|
||||
}
|
||||
|
||||
export const FeedColumnSettings: React.FC<{
|
||||
columnId: string | undefined;
|
||||
localOnly?: boolean;
|
||||
}> = ({ columnId, localOnly }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const settings = useColumnSettings(columnId);
|
||||
|
||||
const onChange = useCallback<MenuItemCheckboxChangeHandler>(
|
||||
({ value, checked }) => {
|
||||
@@ -47,12 +52,8 @@ export const FeedColumnSettings: React.FC<{
|
||||
[columnId, dispatch],
|
||||
);
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
// @ts-expect-error settings isn't typed yet
|
||||
const onlyMedia = settings.getIn(['other', 'onlyMedia']) as boolean;
|
||||
// @ts-expect-error settings isn't typed yet
|
||||
const onlyRemote = settings.getIn(['other', 'onlyRemote']) as boolean;
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-call */
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -110,6 +110,7 @@ export const MODAL_COMPONENTS = {
|
||||
'COMPOSER_REARRANGE': () => import('@/mastodon/features/compose/redesign/modal_rearrange'),
|
||||
'COMPOSER_SWITCH_TO_POST': () => import('@/mastodon/features/compose/redesign/modal_switch'),
|
||||
'NOTIFICATION_SETTINGS': () => import('@/mastodon/features/notifications_v2/components/notification_settings_modal'),
|
||||
'HASHTAG_SETTINGS': () => import('@/mastodon/features/hashtag_timeline/components/column_settings_modal'),
|
||||
};
|
||||
|
||||
/** @arg {keyof import('@/mastodon/features/account_edit/modals')} type */
|
||||
|
||||
@@ -868,8 +868,11 @@
|
||||
"hashtag.feature": "Feature on profile",
|
||||
"hashtag.follow": "Follow hashtag",
|
||||
"hashtag.mute": "Mute #{hashtag}",
|
||||
"hashtag.options": "Hashtag options",
|
||||
"hashtag.unfeature": "Don't feature on profile",
|
||||
"hashtag.unfollow": "Unfollow hashtag",
|
||||
"hashtags.add_more_tags": "Add more tags to this column…",
|
||||
"hashtags.add_more_tags_to_column": "Add more tags to #{tag}",
|
||||
"hashtags.and_other": "…and {count, plural, other {# more}}",
|
||||
"hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",
|
||||
"hints.profiles.follows_may_be_missing": "Follows for this profile may be missing.",
|
||||
|
||||
Reference in New Issue
Block a user