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 { import {
CreateFilter, CreateFilter,
LanguageFilter, LanguageFilter,
MediaFilter,
PlatformFilters, PlatformFilters,
ToggleFilter, ToggleRepliesFilter,
generateFilter, generateFilter,
} from 'soapbox/features/explorer/components/filters.tsx'; } from 'soapbox/features/explorer/components/filters.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts'; import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
import { IFilters } from 'soapbox/reducers/search-filter.ts';
const messages = defineMessages({ const messages = defineMessages({
filters: { id: 'column.explorer.filters', defaultMessage: 'Filters:' }, filters: { id: 'column.explorer.filters', defaultMessage: 'Filters:' },
@ -28,6 +30,15 @@ interface IGenerateFilter {
value: string; 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 ExplorerFilter = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const filters = useAppSelector((state) => state.search_filter); const filters = useAppSelector((state) => state.search_filter);
@ -36,14 +47,7 @@ const ExplorerFilter = () => {
useEffect( useEffect(
() => { () => {
const language = filters[0].name.toLowerCase() !== 'default' ? filters[0].value : ''; const value = formatFilters(filters);
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(' ');
dispatch(changeSearch(value)); dispatch(changeSearch(value));
dispatch(submitSearch(undefined, value)); dispatch(submitSearch(undefined, value));
@ -60,7 +64,7 @@ const ExplorerFilter = () => {
{intl.formatMessage(messages.filters)} {intl.formatMessage(messages.filters)}
</Text> </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> </HStack>
<IconButton <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}> <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 */} {/* Show Reply toggle */}
<ToggleFilter type='reply' /> <ToggleRepliesFilter />
{/* Media toggle */} {/* Media toggle */}
<ToggleFilter type='media' /> <MediaFilter />
{/* Video toggle */}
<ToggleFilter type='video' />
{/* Language */} {/* Language */}
<LanguageFilter /> <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 xIcon from '@tabler/icons/outline/x.svg';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; 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 Button from 'soapbox/components/ui/button.tsx';
import Checkbox from 'soapbox/components/ui/checkbox.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 { SelectDropdown } from 'soapbox/features/forms/index.tsx';
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts'; import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
import { useAppSelector } from 'soapbox/hooks/useAppSelector.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 { AppDispatch, RootState } from 'soapbox/store.ts';
import toast from 'soapbox/toast.tsx'; import toast from 'soapbox/toast.tsx';
const messages = defineMessages({ const messages = defineMessages({
showReplies: { id: 'column.explorer.filters.show_replies', defaultMessage: 'Show replies:' }, noReplies: { id: 'column.explorer.filters.no_replies', defaultMessage: 'No Replies:' },
showMedia: { id: 'column.explorer.filters.show_text_posts', defaultMessage: 'Just text posts:' }, media: { id: 'column.explorer.filters.media', defaultMessage: 'Media:' },
showVideo: { id: 'column.explorer.filters.show_video_posts', defaultMessage: 'Just posts with video:' },
language: { id: 'column.explorer.filters.language', defaultMessage: 'Language:' }, language: { id: 'column.explorer.filters.language', defaultMessage: 'Language:' },
platforms: { id: 'column.explorer.filters.platforms', defaultMessage: 'Platforms:' }, platforms: { id: 'column.explorer.filters.platforms', defaultMessage: 'Platforms:' },
createYourFilter: { id: 'column.explorer.filters.create_your_filter', defaultMessage: 'Create your filter' }, 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' }, fediverse: { id: 'column.explorer.filters.fediverse', defaultMessage: 'Fediverse' },
cancel: { id: 'column.explorer.filters.cancel', defaultMessage: 'Cancel' }, cancel: { id: 'column.explorer.filters.cancel', defaultMessage: 'Cancel' },
addFilter: { id: 'column.explorer.filters.add_filter', defaultMessage: 'Add Filter' }, 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 = { const languages = {
@ -133,7 +136,7 @@ const PlatformFilters = () => {
checked={checked} checked={checked}
onChange={handleProtocolFilter} onChange={handleProtocolFilter}
/> />
<Text size='lg'> <Text size='md'>
{intl.formatMessage(message)} {intl.formatMessage(message)}
</Text> </Text>
</HStack> </HStack>
@ -142,7 +145,7 @@ const PlatformFilters = () => {
return ( return (
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}> <HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
<Text size='lg' weight='bold'> <Text size='md' weight='bold'>
{intl.formatMessage(messages.platforms)} {intl.formatMessage(messages.platforms)}
</Text> </Text>
@ -178,12 +181,12 @@ const CreateFilter = () => {
return ( return (
<Stack space={3}> <Stack space={3}>
<Text size='lg' weight='bold'> <Text size='md' weight='bold'>
{intl.formatMessage(messages.createYourFilter)} {intl.formatMessage(messages.createYourFilter)}
</Text> </Text>
<Stack> <Stack space={2}>
<Text size='lg'> <Text size='md'>
{intl.formatMessage(messages.filterByWords)} {intl.formatMessage(messages.filterByWords)}
</Text> </Text>
@ -191,7 +194,7 @@ const CreateFilter = () => {
<div className='relative w-full items-center'> <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 <div
role='button' role='button'
tabIndex={0} tabIndex={0}
@ -220,7 +223,7 @@ const CreateFilter = () => {
setInclude(true); setInclude(true);
}} }}
/> />
<Text size='lg'> <Text size='md'>
{intl.formatMessage(messages.include)} {intl.formatMessage(messages.include)}
</Text> </Text>
</HStack> </HStack>
@ -234,7 +237,7 @@ const CreateFilter = () => {
setInclude(false); setInclude(false);
}} }}
/> />
<Text size='lg'> <Text size='md'>
{intl.formatMessage(messages.exclude)} {intl.formatMessage(messages.exclude)}
</Text> </Text>
</HStack> </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 LanguageFilter = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const languageFilter = useAppSelector((state) => state.search_filter)[0];
const defaultValue = languageFilter.name.toLowerCase();
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => { const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
const language = e.target.value; const language = e.target.value;
@ -273,14 +316,14 @@ const LanguageFilter = () => {
return ( return (
<HStack alignItems='center' space={2}> <HStack alignItems='center' space={2}>
<Text size='lg' weight='bold'> <Text size='md' weight='bold'>
{intl.formatMessage(messages.language)} {intl.formatMessage(messages.language)}
</Text> </Text>
<SelectDropdown <SelectDropdown
className='max-w-[200px]' className='max-w-[130px]'
items={languages} items={languages}
defaultValue={languages.default} defaultValue={defaultValue}
onChange={handleSelectChange} onChange={handleSelectChange}
/> />
</HStack> </HStack>
@ -288,39 +331,27 @@ const LanguageFilter = () => {
}; };
const ToggleFilter = ({ type }: {type: 'reply' | 'media' | 'video'}) => { const ToggleRepliesFilter = () => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const filterType = type.toLowerCase(); const label = intl.formatMessage(messages.noReplies);
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 filters = useAppSelector((state) => state.search_filter); 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 checked = repliesFilter?.status;
const handleToggleComponent = () => { const handleToggle = () => {
dispatch(handleToggle({ type: filterType, checked: !checked })); dispatch(handleToggleReplies({ checked: !checked }));
}; };
return ( return (
<HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}> <HStack className='flex-wrap whitespace-normal' alignItems='center' space={2}>
<Text size='lg' weight='bold'> <Text size='md' weight='bold'>
{label} {label}
</Text> </Text>
<Toggle <Toggle
checked={checked} checked={checked}
onChange={handleToggleComponent} onChange={handleToggle}
/> />
</HStack> </HStack>
); );
@ -341,9 +372,10 @@ const generateFilter = (dispatch: AppDispatch, { name, status }: IGenerateFilter
textColor = 'text-gray-500'; textColor = 'text-gray-500';
} else { } else {
switch (nameLowCase) { switch (nameLowCase) {
case 'reply': case 'no replies':
case 'media': case 'text only':
case 'video': case 'video only':
case 'no media':
borderColor = 'border-gray-500'; borderColor = 'border-gray-500';
textColor = 'text-gray-500'; textColor = 'text-gray-500';
break; break;
@ -371,7 +403,7 @@ const generateFilter = (dispatch: AppDispatch, { name, status }: IGenerateFilter
key={name} 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} `} 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 {hasButton && <IconButton
iconClassName='!w-4' className={`hidden !p-0 px-1 group-hover:block ${textColor}`} src={xIcon} iconClassName='!w-4' className={`hidden !p-0 px-1 group-hover:block ${textColor}`} src={xIcon}
onClick={handleChangeFilters} 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.filter_by_words": "Filter by this/these words",
"column.explorer.filters.include": "Include", "column.explorer.filters.include": "Include",
"column.explorer.filters.language": "Language:", "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.no_replies": "No Replies:",
"column.explorer.filters.nostr": "Nostr", "column.explorer.filters.nostr": "Nostr",
"column.explorer.filters.platforms": "Platforms:", "column.explorer.filters.platforms": "Platforms:",
"column.explorer.filters.show_text_posts": "Just text posts:", "column.explorer.media_filters.all": "All",
"column.explorer.filters.show_video_posts": "Just posts with video:", "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.nostr": "Nostr",
"column.explorer.popular_accounts": "Popular Accounts", "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.", "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'; import { PayloadAction, createSlice } from '@reduxjs/toolkit';
interface IFilters { interface IFilters {
name: string; // The name of the filter. name: string;
status: boolean; // Whether the filter is active or not. status: boolean;
value: string; // The filter value used for searching. value: string;
} }
interface IToggle { interface IToggle {
type: string; // The filter type to toggle. checked: boolean;
checked: boolean; // The new status of the filter.
} }
interface INewFilter { interface INewFilter {
name: string; // The name of the new filter. name: string;
status: boolean; // Whether the filter should be active by default. status: boolean;
} }
const initialState: IFilters[] = [ const initialState: IFilters[] = [
@ -21,9 +20,10 @@ const initialState: IFilters[] = [
{ name: 'Nostr', status: true, value: 'protocol:nostr' }, { name: 'Nostr', status: true, value: 'protocol:nostr' },
{ name: 'Bluesky', status: true, value: 'protocol:atproto' }, { name: 'Bluesky', status: true, value: 'protocol:atproto' },
{ name: 'Fediverse', status: true, value: 'protocol:activitypub' }, { name: 'Fediverse', status: true, value: 'protocol:activitypub' },
{ name: 'Reply', status: false, value: 'reply:true' }, { name: 'No Replies', status: false, value: 'reply:false' },
{ name: 'Media', status: false, value: 'media:true' }, { name: 'Video Only', status: false, value: 'video:true' },
{ name: 'Video', 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({ const search_filter = createSlice({
@ -31,22 +31,58 @@ const search_filter = createSlice({
initialState, initialState,
reducers: { 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) => { return state.map((currentState) => {
const checked = action.payload.checked; const checked = action.payload.checked;
const type = action.payload.type.toLowerCase(); return currentState.value.toLowerCase().includes('reply')
return currentState.name.toLowerCase() === type
? { ? {
...currentState, ...currentState,
status: checked, status: checked,
value: `${type}:${checked}`,
} }
: currentState; : 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. * 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; export default search_filter.reducer;