diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.jsx b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.jsx
index 18222a44ffd..6322dd965e1 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.jsx
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings.jsx
@@ -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 (
+
+ {this.modeSelect('any')}
+ {this.modeSelect('all')}
+ {this.modeSelect('none')}
+
+ )
+ }
+
return (
@@ -115,7 +126,7 @@ class ColumnSettings extends PureComponent {
} />
-
+
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/column_settings_modal.tsx b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings_modal.tsx
new file mode 100644
index 00000000000..246d7e2b08e
--- /dev/null
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/column_settings_modal.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default HashtagSettingsModal;
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_column_menu.tsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_column_menu.tsx
new file mode 100644
index 00000000000..12807911834
--- /dev/null
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_column_menu.tsx
@@ -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(
+ ({ 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 (
+
+ }
+ >
+
+
+ {multiColumn && pinned && (
+ <>
+
+
+
+
+
+ >
+ )}
+ {multiColumn && (
+
+ )}
+
+ );
+};
diff --git a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
index fafe186cb41..a99f2ccbc0f 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
+++ b/app/javascript/mastodon/features/hashtag_timeline/components/hashtag_header.tsx
@@ -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();
@@ -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 && (