Resize text and create "media filter"

merge-requests/3337/head
danidfra 2025-02-28 03:52:57 -03:00
rodzic dabac1a502
commit f7e442cf4a
4 zmienionych plików z 147 dodań i 72 usunięć

Wyświetl plik

@ -11,12 +11,14 @@ import Text from 'soapbox/components/ui/text.tsx';
import {
CreateFilter,
LanguageFilter,
MediaFilter,
PlatformFilters,
ToggleFilter,
ToggleRepliesFilter,
generateFilter,
} from 'soapbox/features/explorer/components/filters.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { IFilters } from 'soapbox/reducers/search-filter.ts';
const messages = defineMessages({
filters: { id: 'column.explorer.filters', defaultMessage: 'Filters:' },
@ -28,6 +30,15 @@ interface IGenerateFilter {
value: string;
}
export const formatFilters = (filters: IFilters[]): string => {
const language = filters[0].name.toLowerCase() !== 'default' ? filters[0].value : '';
const protocols = filters.slice(1, 4).filter((protocol) => !protocol.status).map((filter) => filter.value).join(' ');
const defaultFilters = filters.slice(4, 8).filter((x) => x.status).map((filter) => filter.value).join(' ');
const newFilters = filters.slice(8).map((searchFilter) => searchFilter.value).join(' ');
return [language, protocols, defaultFilters, newFilters].join(' ').trim();
};
const ExplorerFilter = () => {
const dispatch = useAppDispatch();
const filters = useAppSelector((state) => state.search_filter);
@ -36,14 +47,7 @@ const ExplorerFilter = () => {
useEffect(
() => {
const language = filters[0].name.toLowerCase() !== 'default' ? filters[0].value : '';
const protocols = filters.slice(1, 4).filter((protocol) => !protocol.status).map((filter) => filter.value).join(' ');
const defaultFilters = filters.slice(4, 7).filter((x) => x.status).map((filter) => filter.value).join(' ');
const newFilters = filters.slice(7)
.map((searchFilter) => searchFilter.value)
.join(' ');
const value = [ language, protocols, defaultFilters, newFilters ].join(' ');
const value = formatFilters(filters);
dispatch(changeSearch(value));
dispatch(submitSearch(undefined, value));
@ -60,7 +64,7 @@ const ExplorerFilter = () => {
{intl.formatMessage(messages.filters)}
</Text>
{filters.length > 0 && [...filters.slice(0, 7).filter((value) => value.status).map((value) => generateFilter(dispatch, value)), ...filters.slice(7).map((value) => generateFilter(dispatch, value))]}
{filters.length > 0 && [...filters.slice(0, 8).filter((value) => value.status).map((value) => generateFilter(dispatch, value)), ...filters.slice(8).map((value) => generateFilter(dispatch, value))]}
</HStack>
<IconButton
@ -74,13 +78,11 @@ const ExplorerFilter = () => {
<Stack className={`overflow-hidden transition-all duration-500 ease-in-out ${isOpen ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0'}`} space={3}>
{/* Show Reply toggle */}
<ToggleFilter type='reply' />
<ToggleRepliesFilter />
{/* Media toggle */}
<ToggleFilter type='media' />
<MediaFilter />
{/* Video toggle */}
<ToggleFilter type='video' />
{/* Language */}
<LanguageFilter />

Wyświetl plik

@ -2,7 +2,7 @@ import searchIcon from '@tabler/icons/outline/search.svg';
import xIcon from '@tabler/icons/outline/x.svg';
import clsx from 'clsx';
import { useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
import Button from 'soapbox/components/ui/button.tsx';
import Checkbox from 'soapbox/components/ui/checkbox.tsx';
@ -17,14 +17,13 @@ import { IGenerateFilter } from 'soapbox/features/explorer/components/explorerFi
import { SelectDropdown } from 'soapbox/features/forms/index.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { changeLanguage, createFilter, handleToggle, removeFilter, selectProtocol } from 'soapbox/reducers/search-filter.ts';
import { changeLanguage, changeMedia, createFilter, handleToggleReplies, removeFilter, selectProtocol } from 'soapbox/reducers/search-filter.ts';
import { AppDispatch, RootState } from 'soapbox/store.ts';
import toast from 'soapbox/toast.tsx';
const messages = defineMessages({
showReplies: { id: 'column.explorer.filters.show_replies', defaultMessage: 'Show replies:' },
showMedia: { id: 'column.explorer.filters.show_text_posts', defaultMessage: 'Just text posts:' },
showVideo: { id: 'column.explorer.filters.show_video_posts', defaultMessage: 'Just posts with video:' },
noReplies: { id: 'column.explorer.filters.no_replies', defaultMessage: 'No Replies:' },
media: { id: 'column.explorer.filters.media', defaultMessage: 'Media:' },
language: { id: 'column.explorer.filters.language', defaultMessage: 'Language:' },
platforms: { id: 'column.explorer.filters.platforms', defaultMessage: 'Platforms:' },
createYourFilter: { id: 'column.explorer.filters.create_your_filter', defaultMessage: 'Create your filter' },
@ -36,6 +35,10 @@ const messages = defineMessages({
fediverse: { id: 'column.explorer.filters.fediverse', defaultMessage: 'Fediverse' },
cancel: { id: 'column.explorer.filters.cancel', defaultMessage: 'Cancel' },
addFilter: { id: 'column.explorer.filters.add_filter', defaultMessage: 'Add Filter' },
all: { id: 'column.explorer.media_filters.all', defaultMessage: 'All' },
textOnly: { id: 'column.explorer.media_filters.text', defaultMessage: 'Text only' },
videoOnly: { id: 'column.explorer.media_filters.video', defaultMessage: 'Video only' },
none: { id: 'column.explorer.media_filters.none', defaultMessage: 'No media' },
});
const languages = {
@ -133,7 +136,7 @@ const PlatformFilters = () => {
checked={checked}
onChange={handleProtocolFilter}
/>
<Text size='lg'>
<Text size='md'>
{intl.formatMessage(message)}
</Text>
</HStack>
@ -142,7 +145,7 @@ const PlatformFilters = () => {
return (
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
<Text size='lg' weight='bold'>
<Text size='md' weight='bold'>
{intl.formatMessage(messages.platforms)}
</Text>
@ -178,12 +181,12 @@ const CreateFilter = () => {
return (
<Stack space={3}>
<Text size='lg' weight='bold'>
<Text size='md' weight='bold'>
{intl.formatMessage(messages.createYourFilter)}
</Text>
<Stack>
<Text size='lg'>
<Stack space={2}>
<Text size='md'>
{intl.formatMessage(messages.filterByWords)}
</Text>
@ -191,7 +194,7 @@ const CreateFilter = () => {
<div className='relative w-full items-center'>
<Input theme='search' value={inputValue} onChange={(e) => setInputValue(e.target.value)} />
<Input theme='search' value={inputValue} className='h-9' onChange={(e) => setInputValue(e.target.value)} />
<div
role='button'
tabIndex={0}
@ -220,7 +223,7 @@ const CreateFilter = () => {
setInclude(true);
}}
/>
<Text size='lg'>
<Text size='md'>
{intl.formatMessage(messages.include)}
</Text>
</HStack>
@ -234,7 +237,7 @@ const CreateFilter = () => {
setInclude(false);
}}
/>
<Text size='lg'>
<Text size='md'>
{intl.formatMessage(messages.exclude)}
</Text>
</HStack>
@ -262,9 +265,49 @@ const CreateFilter = () => {
};
const MediaFilter = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const filters = useAppSelector((state) => state.search_filter).slice(4, 8);
const mediaFilters = {
all: intl.formatMessage(messages.all),
text: intl.formatMessage(messages.textOnly),
video: intl.formatMessage(messages.videoOnly),
none: intl.formatMessage(messages.none),
};
const defaultValue = (Object.keys(mediaFilters) as Array<keyof typeof mediaFilters>).find((key) => mediaFilters[key] === filters.find((filter) => filter.status === true)?.name) || mediaFilters.all;
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
const filter = e.target.value;
dispatch(changeMedia(filter));
};
return (
<HStack alignItems='center' space={2}>
<Text size='md' weight='bold'>
{intl.formatMessage(messages.media)}
</Text>
<SelectDropdown
className='max-w-[130px]'
items={mediaFilters}
defaultValue={defaultValue}
onChange={handleSelectChange}
/>
</HStack>
);
};
const LanguageFilter = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const languageFilter = useAppSelector((state) => state.search_filter)[0];
const defaultValue = languageFilter.name.toLowerCase();
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
const language = e.target.value;
@ -273,14 +316,14 @@ const LanguageFilter = () => {
return (
<HStack alignItems='center' space={2}>
<Text size='lg' weight='bold'>
<Text size='md' weight='bold'>
{intl.formatMessage(messages.language)}
</Text>
<SelectDropdown
className='max-w-[200px]'
className='max-w-[130px]'
items={languages}
defaultValue={languages.default}
defaultValue={defaultValue}
onChange={handleSelectChange}
/>
</HStack>
@ -288,39 +331,27 @@ const LanguageFilter = () => {
};
const ToggleFilter = ({ type }: {type: 'reply' | 'media' | 'video'}) => {
const ToggleRepliesFilter = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const filterType = type.toLowerCase();
let label;
switch (type) {
case 'reply':
label = intl.formatMessage(messages.showReplies);
break;
case 'media':
label = intl.formatMessage(messages.showMedia);
break;
default:
label = intl.formatMessage(messages.showVideo);
}
const label = intl.formatMessage(messages.noReplies);
const filters = useAppSelector((state) => state.search_filter);
const repliesFilter = filters.find((filter) => filter.name.toLowerCase() === filterType);
const repliesFilter = filters.find((filter) => filter.value.toLowerCase().includes('reply'));
const checked = repliesFilter?.status;
const handleToggleComponent = () => {
dispatch(handleToggle({ type: filterType, checked: !checked }));
const handleToggle = () => {
dispatch(handleToggleReplies({ checked: !checked }));
};
return (
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
<Text size='lg' weight='bold'>
<Text size='md' weight='bold'>
{label}
</Text>
<Toggle
checked={checked}
onChange={handleToggleComponent}
onChange={handleToggle}
/>
</HStack>
);
@ -341,9 +372,10 @@ const generateFilter = (dispatch: AppDispatch, { name, status }: IGenerateFilter
textColor = 'text-gray-500';
} else {
switch (nameLowCase) {
case 'reply':
case 'media':
case 'video':
case 'no replies':
case 'text only':
case 'video only':
case 'no media':
borderColor = 'border-gray-500';
textColor = 'text-gray-500';
break;
@ -371,7 +403,7 @@ const generateFilter = (dispatch: AppDispatch, { name, status }: IGenerateFilter
key={name}
className={`group m-1 flex items-center gap-0.5 whitespace-normal break-words rounded-full border-2 bg-transparent px-3 text-base font-medium shadow-sm hover:cursor-pointer ${hasButton ? 'hover:pr-1' : '' } ${borderColor} ${textColor} `}
>
{name}
{name.toLowerCase() !== 'default' ? name : <FormattedMessage id='column.explorer.filters.language.default' defaultMessage='Global' />}
{hasButton && <IconButton
iconClassName='!w-4' className={`hidden !p-0 px-1 group-hover:block ${textColor}`} src={xIcon}
onClick={handleChangeFilters}
@ -381,4 +413,4 @@ const generateFilter = (dispatch: AppDispatch, { name, status }: IGenerateFilter
);
};
export { CreateFilter, PlatformFilters, LanguageFilter, ToggleFilter, generateFilter };
export { CreateFilter, PlatformFilters, MediaFilter, LanguageFilter, ToggleRepliesFilter, generateFilter };

Wyświetl plik

@ -380,11 +380,15 @@
"column.explorer.filters.filter_by_words": "Filter by this/these words",
"column.explorer.filters.include": "Include",
"column.explorer.filters.language": "Language:",
"column.explorer.filters.language.default": "Global",
"column.explorer.filters.media": "Media:",
"column.explorer.filters.no_replies": "No Replies:",
"column.explorer.filters.nostr": "Nostr",
"column.explorer.filters.platforms": "Platforms:",
"column.explorer.filters.show_text_posts": "Just text posts:",
"column.explorer.filters.show_video_posts": "Just posts with video:",
"column.explorer.media_filters.all": "All",
"column.explorer.media_filters.none": "No media",
"column.explorer.media_filters.text": "Text only",
"column.explorer.media_filters.video": "Video only",
"column.explorer.nostr": "Nostr",
"column.explorer.popular_accounts": "Popular Accounts",
"column.explorer.welcome_card.text": "Explore the world of decentralized social media, dive into {nostrLink} or cross {bridgeLink} to other networks, and connect with a global community. All in one place.",

Wyświetl plik

@ -1,19 +1,18 @@
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
interface IFilters {
name: string; // The name of the filter.
status: boolean; // Whether the filter is active or not.
value: string; // The filter value used for searching.
name: string;
status: boolean;
value: string;
}
interface IToggle {
type: string; // The filter type to toggle.
checked: boolean; // The new status of the filter.
checked: boolean;
}
interface INewFilter {
name: string; // The name of the new filter.
status: boolean; // Whether the filter should be active by default.
name: string;
status: boolean;
}
const initialState: IFilters[] = [
@ -21,9 +20,10 @@ const initialState: IFilters[] = [
{ name: 'Nostr', status: true, value: 'protocol:nostr' },
{ name: 'Bluesky', status: true, value: 'protocol:atproto' },
{ name: 'Fediverse', status: true, value: 'protocol:activitypub' },
{ name: 'Reply', status: false, value: 'reply:true' },
{ name: 'Media', status: false, value: 'media:true' },
{ name: 'Video', status: false, value: 'video:true' },
{ name: 'No Replies', status: false, value: 'reply:false' },
{ name: 'Video Only', status: false, value: 'video:true' },
{ name: 'Image Only', status: false, value: 'media:true -video:true' },
{ name: 'No media', status: false, value: '-media:true' },
];
const search_filter = createSlice({
@ -31,22 +31,58 @@ const search_filter = createSlice({
initialState,
reducers: {
/**
* Toggles the status of a filter.
* Toggles the status of reply filter.
*/
handleToggle: (state, action: PayloadAction<IToggle>) => {
handleToggleReplies: (state, action: PayloadAction<IToggle>) => {
return state.map((currentState) => {
const checked = action.payload.checked;
const type = action.payload.type.toLowerCase();
return currentState.name.toLowerCase() === type
return currentState.value.toLowerCase().includes('reply')
? {
...currentState,
status: checked,
value: `${type}:${checked}`,
}
: currentState;
});
},
/**
* Changes the media filter.
*/
changeMedia: (state, action: PayloadAction<string>) => {
const selected = action.payload.toLowerCase();
const resetMediaState = state.map((currentFilter, index) => {
return index > 3 && index <= 7
? {
...currentFilter,
status: false,
}
: currentFilter;
});
const applyMediaFilter = (searchFilter: string) => {
return resetMediaState.map((currentState) =>
currentState.name.toLowerCase().includes(searchFilter)
? {
...currentState,
status: true,
}
: currentState,
);
};
switch (selected) {
case 'text':
case 'video':
case 'image':
return applyMediaFilter(`${selected} only`);
case 'none':
return applyMediaFilter('no media');
default:
return resetMediaState;
}
},
/**
* Changes the language filter.
*/
@ -105,5 +141,6 @@ const search_filter = createSlice({
},
});
export const { handleToggle, changeLanguage, selectProtocol, createFilter, removeFilter, resetFilters } = search_filter.actions;
export type { IFilters };
export const { handleToggleReplies, changeMedia, changeLanguage, selectProtocol, createFilter, removeFilter, resetFilters } = search_filter.actions;
export default search_filter.reducer;