kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Resize text and create "media filter"
rodzic
dabac1a502
commit
f7e442cf4a
|
@ -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 />
|
||||
|
|
|
@ -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 };
|
|
@ -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.",
|
||||
|
|
|
@ -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;
|
Ładowanie…
Reference in New Issue