Collection editor: Format topic as hashtag (#38153)

This commit is contained in:
diondiondion 2026-03-11 18:04:37 +01:00 committed by GitHub
parent 3091e2e525
commit 3ef7d2835a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 81 additions and 17 deletions

View File

@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
import { isFulfilled } from '@reduxjs/toolkit';
import { inputToHashtag } from '@/mastodon/utils/hashtags';
import type {
ApiCreateCollectionPayload,
ApiUpdateCollectionPayload,
@ -64,7 +65,7 @@ export const CollectionDetails: React.FC = () => {
dispatch(
updateCollectionEditorField({
field: 'topic',
value: event.target.value,
value: inputToHashtag(event.target.value),
}),
);
},
@ -219,6 +220,9 @@ export const CollectionDetails: React.FC = () => {
}
value={topic}
onChange={handleTopicChange}
autoCapitalize='off'
autoCorrect='off'
spellCheck='false'
maxLength={40}
/>

View File

@ -37,30 +37,30 @@ interface CollectionState {
status: QueryStatus;
}
>;
editor: {
id: string | undefined;
name: string;
description: string;
topic: string;
language: string | null;
discoverable: boolean;
sensitive: boolean;
accountIds: string[];
};
editor: EditorState;
}
type EditorField = CollectionState['editor'];
interface EditorState {
id: string | null;
name: string;
description: string;
topic: string;
language: string | null;
discoverable: boolean;
sensitive: boolean;
accountIds: string[];
}
interface UpdateEditorFieldPayload<K extends keyof EditorField> {
interface UpdateEditorFieldPayload<K extends keyof EditorState> {
field: K;
value: EditorField[K];
value: EditorState[K];
}
const initialState: CollectionState = {
collections: {},
accountCollections: {},
editor: {
id: undefined,
id: null,
name: '',
description: '',
topic: '',
@ -79,7 +79,7 @@ const collectionSlice = createSlice({
const collection = action.payload;
state.editor = {
id: collection?.id,
id: collection?.id ?? null,
name: collection?.name ?? '',
description: collection?.description ?? '',
topic: collection?.tag?.name ?? '',
@ -92,7 +92,7 @@ const collectionSlice = createSlice({
reset(state) {
state.editor = initialState.editor;
},
updateEditorField<K extends keyof EditorField>(
updateEditorField<K extends keyof EditorState>(
state: CollectionState,
action: PayloadAction<UpdateEditorFieldPayload<K>>,
) {

View File

@ -0,0 +1,28 @@
import { inputToHashtag } from './hashtags';
describe('inputToHashtag', () => {
test.concurrent.each([
['', ''],
// Prepend or keep hashtag
['mastodon', '#mastodon'],
['#mastodon', '#mastodon'],
// Preserve trailing whitespace
['mastodon ', '#mastodon '],
[' ', '# '],
// Collapse whitespace & capitalise first character
['cats of mastodon', '#catsOfMastodon'],
['x y z', '#xYZ'],
[' mastodon', '#mastodon'],
// Preserve initial casing
['Log in', '#LogIn'],
['#NaturePhotography', '#NaturePhotography'],
// Normalise hash symbol variant
['nature', '#nature'],
['Nature Photography', '#NaturePhotography'],
// Allow special characters
['hello-world', '#hello-world'],
['hello,world', '#hello,world'],
])('for input "%s", return "%s"', (input, expected) => {
expect(inputToHashtag(input)).toBe(expected);
});
});

View File

@ -27,3 +27,35 @@ const buildHashtagRegex = () => {
export const HASHTAG_PATTERN_REGEX = buildHashtagPatternRegex();
export const HASHTAG_REGEX = buildHashtagRegex();
/**
* Formats an input string as a hashtag:
* - Prepends `#` unless present
* - Strips spaces (except at the end, to allow typing it)
* - Capitalises first character after stripped space
*/
export const inputToHashtag = (input: string): string => {
if (!input) {
return '';
}
const trailingSpace = /\s+$/.exec(input)?.[0] ?? '';
const trimmedInput = input.trimEnd();
const withoutHash =
trimmedInput.startsWith('#') || trimmedInput.startsWith('')
? trimmedInput.slice(1)
: trimmedInput;
// Split by space, filter empty strings, and capitalise the start of each word but the first
const words = withoutHash
.split(/\s+/)
.filter((word) => word.length > 0)
.map((word, index) =>
index === 0
? word
: word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
);
return `#${words.join('')}${trailingSpace}`;
};