mirror of
https://github.com/mastodon/mastodon.git
synced 2026-03-21 18:05:23 -05:00
Collection editor: Format topic as hashtag (#38153)
This commit is contained in:
parent
3091e2e525
commit
3ef7d2835a
|
|
@ -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}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
) {
|
||||
|
|
|
|||
28
app/javascript/mastodon/utils/hashtags.test.ts
Normal file
28
app/javascript/mastodon/utils/hashtags.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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}`;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user