Fix filters, restyle filters page
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
d44be7fbf8
commit
49a7d40efb
13 changed files with 99 additions and 194 deletions
|
@ -289,8 +289,10 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={minHandlers}>
|
||||
<div className={clsx('status__wrapper', 'status__wrapper--filtered', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
<div className={clsx('status__wrapper text-center', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
</Text>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
|
|
@ -6,8 +6,7 @@ import { makeGetStatus } from 'soapbox/selectors';
|
|||
|
||||
interface IStatusContainer extends Omit<IStatus, 'status'> {
|
||||
id: string,
|
||||
/** @deprecated Unused. */
|
||||
contextType?: any,
|
||||
contextType?: string,
|
||||
/** @deprecated Unused. */
|
||||
otherAccounts?: any,
|
||||
/** @deprecated Unused. */
|
||||
|
@ -21,10 +20,10 @@ interface IStatusContainer extends Omit<IStatus, 'status'> {
|
|||
* @deprecated Use the Status component directly.
|
||||
*/
|
||||
const StatusContainer: React.FC<IStatusContainer> = (props) => {
|
||||
const { id, ...rest } = props;
|
||||
const { id, contextType, ...rest } = props;
|
||||
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector(state => getStatus(state, { id }));
|
||||
const status = useAppSelector(state => getStatus(state, { id, contextType }));
|
||||
|
||||
if (status) {
|
||||
return <Status status={status} {...rest} />;
|
||||
|
|
|
@ -2,13 +2,9 @@ import React, { useEffect, useState } from 'react';
|
|||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input, Text } from 'soapbox/components/ui';
|
||||
import {
|
||||
FieldsGroup,
|
||||
Checkbox,
|
||||
} from 'soapbox/features/forms';
|
||||
import { Button, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, HStack, IconButton, Input, Stack, Text, Toggle } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
|
@ -33,6 +29,13 @@ const messages = defineMessages({
|
|||
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const contexts = {
|
||||
home: messages.home_timeline,
|
||||
public: messages.public_timeline,
|
||||
notifications: messages.notifications,
|
||||
thread: messages.conversations,
|
||||
};
|
||||
|
||||
// const expirations = {
|
||||
// null: 'Never',
|
||||
// // 3600: '30 minutes',
|
||||
|
@ -85,8 +88,8 @@ const Filters = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleFilterDelete: React.MouseEventHandler<HTMLDivElement> = e => {
|
||||
dispatch(deleteFilter(e.currentTarget.dataset.value!)).then(() => {
|
||||
const handleFilterDelete = (id: string) => () => {
|
||||
dispatch(deleteFilter(id)).then(() => {
|
||||
return dispatch(fetchFilters());
|
||||
}).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.delete_error));
|
||||
|
@ -121,58 +124,68 @@ const Filters = () => {
|
|||
/>
|
||||
</FormGroup> */}
|
||||
|
||||
<FieldsGroup>
|
||||
<Text tag='label'>
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium'>
|
||||
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
|
||||
</Text>
|
||||
<Text theme='muted' size='xs'>
|
||||
<Text size='xs' theme='muted'>
|
||||
<FormattedMessage id='filters.context_hint' defaultMessage='One or multiple contexts where the filter should apply' />
|
||||
</Text>
|
||||
<div className='two-col'>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.home_timeline)}
|
||||
</Stack>
|
||||
|
||||
<List>
|
||||
<ListItem label={intl.formatMessage(messages.home_timeline)}>
|
||||
<Toggle
|
||||
name='home_timeline'
|
||||
checked={homeTimeline}
|
||||
onChange={({ target }) => setHomeTimeline(target.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.public_timeline)}
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.public_timeline)}>
|
||||
<Toggle
|
||||
name='public_timeline'
|
||||
checked={publicTimeline}
|
||||
onChange={({ target }) => setPublicTimeline(target.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.notifications)}
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.notifications)}>
|
||||
<Toggle
|
||||
name='notifications'
|
||||
checked={notifications}
|
||||
onChange={({ target }) => setNotifications(target.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
label={intl.formatMessage(messages.conversations)}
|
||||
</ListItem>
|
||||
<ListItem label={intl.formatMessage(messages.conversations)}>
|
||||
<Toggle
|
||||
name='conversations'
|
||||
checked={conversations}
|
||||
onChange={({ target }) => setConversations(target.checked)}
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</FieldsGroup>
|
||||
|
||||
<FieldsGroup>
|
||||
<Checkbox
|
||||
<List>
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.drop_header)}
|
||||
hint={intl.formatMessage(messages.drop_hint)}
|
||||
name='irreversible'
|
||||
checked={irreversible}
|
||||
onChange={({ target }) => setIrreversible(target.checked)}
|
||||
/>
|
||||
<Checkbox
|
||||
>
|
||||
<Toggle
|
||||
name='irreversible'
|
||||
checked={irreversible}
|
||||
onChange={({ target }) => setIrreversible(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.whole_word_header)}
|
||||
hint={intl.formatMessage(messages.whole_word_hint)}
|
||||
name='whole_word'
|
||||
checked={wholeWord}
|
||||
onChange={({ target }) => setWholeWord(target.checked)}
|
||||
/>
|
||||
</FieldsGroup>
|
||||
>
|
||||
<Toggle
|
||||
name='whole_word'
|
||||
checked={wholeWord}
|
||||
onChange={({ target }) => setWholeWord(target.checked)}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
|
||||
|
@ -186,40 +199,41 @@ const Filters = () => {
|
|||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
>
|
||||
{filters.map((filter, i) => (
|
||||
<div key={i} className='filter__container'>
|
||||
<div className='filter__details'>
|
||||
<div className='filter__phrase'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' /></span>
|
||||
<span className='filter__list-value'>{filter.phrase}</span>
|
||||
</div>
|
||||
<div className='filter__contexts'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
{filter.context.map((context, i) => (
|
||||
<span key={i} className='context'>{context}</span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
<div className='filter__details'>
|
||||
<span className='filter__list-label'><FormattedMessage id='filters.filters_list_details_label' defaultMessage='Filter settings:' /></span>
|
||||
<span className='filter__list-value'>
|
||||
<HStack space={1} justifyContent='between'>
|
||||
<Stack space={1}>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_phrase_label' defaultMessage='Keyword or phrase:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.phrase}</Text>
|
||||
</Text>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')}</Text>
|
||||
</Text>
|
||||
<HStack space={4}>
|
||||
<Text weight='medium'>
|
||||
{filter.irreversible ?
|
||||
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
|
||||
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
|
||||
}
|
||||
{filter.whole_word &&
|
||||
<span><FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' /></span>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='filter__delete' role='button' tabIndex={0} onClick={handleFilterDelete} data-value={filter.id} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='filter__delete-icon' src={require('@tabler/icons/x.svg')} />
|
||||
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||
</div>
|
||||
</div>
|
||||
<FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /> :
|
||||
<FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' />}
|
||||
</Text>
|
||||
{filter.whole_word && (
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_whole-word' defaultMessage='Whole word' />
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<IconButton
|
||||
iconClassName='h-5 w-5 text-gray-700 dark:text-gray-600 hover:text-gray-800 dark:hover:text-gray-500'
|
||||
src={require('@tabler/icons/trash.svg')}
|
||||
onClick={handleFilterDelete(filter.id)}
|
||||
title={intl.formatMessage(messages.delete)}
|
||||
/>
|
||||
</HStack>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
|
|
|
@ -160,14 +160,6 @@ export const SimpleForm: React.FC<ISimpleForm> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface IFieldsGroup {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export const FieldsGroup: React.FC<IFieldsGroup> = ({ children }) => (
|
||||
<div className='fields-group'>{children}</div>
|
||||
);
|
||||
|
||||
interface ICheckbox {
|
||||
label?: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
|
|
|
@ -329,6 +329,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
|||
onMoveDown={handleMoveDown}
|
||||
onMoveUp={handleMoveUp}
|
||||
avatarSize={avatarSize}
|
||||
contextType='notifications'
|
||||
/>
|
||||
) : null;
|
||||
default:
|
||||
|
|
|
@ -8,6 +8,7 @@ import { useAppSelector } from 'soapbox/hooks';
|
|||
|
||||
interface IThreadStatus {
|
||||
id: string,
|
||||
contextType?: string,
|
||||
focusedStatusId: string,
|
||||
onMoveUp: (id: string) => void,
|
||||
onMoveDown: (id: string) => void,
|
||||
|
|
|
@ -361,6 +361,7 @@ const Thread: React.FC<IThread> = (props) => {
|
|||
focusedStatusId={status!.id}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
contextType='thread'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,11 +5,13 @@
|
|||
*/
|
||||
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
|
||||
|
||||
export type ContextType = 'home' | 'public' | 'notifications' | 'thread';
|
||||
|
||||
// https://docs.joinmastodon.org/entities/filter/
|
||||
export const FilterRecord = ImmutableRecord({
|
||||
id: '',
|
||||
phrase: '',
|
||||
context: ImmutableList<string>(),
|
||||
context: ImmutableList<ContextType>(),
|
||||
whole_word: false,
|
||||
expires_at: '',
|
||||
irreversible: false,
|
||||
|
@ -19,4 +21,4 @@ export const normalizeFilter = (filter: Record<string, any>) => {
|
|||
return FilterRecord(
|
||||
ImmutableMap(fromJS(filter)),
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { validId } from 'soapbox/utils/auth';
|
|||
import ConfigDB from 'soapbox/utils/config-db';
|
||||
import { shouldFilter } from 'soapbox/utils/timelines';
|
||||
|
||||
import type { ContextType } from 'soapbox/normalizers/filter';
|
||||
import type { ReducerChat } from 'soapbox/reducers/chats';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
import type { Filter as FilterEntity, Notification } from 'soapbox/types/entities';
|
||||
|
@ -85,7 +86,7 @@ export const findAccountByUsername = (state: RootState, username: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
const toServerSideType = (columnType: string): string => {
|
||||
const toServerSideType = (columnType: string): ContextType => {
|
||||
switch (columnType) {
|
||||
case 'home':
|
||||
case 'notifications':
|
||||
|
@ -105,10 +106,8 @@ type FilterContext = { contextType?: string };
|
|||
|
||||
export const getFilters = (state: RootState, query: FilterContext) => {
|
||||
return state.filters.filter((filter) => {
|
||||
return query?.contextType
|
||||
&& filter.context.includes(toServerSideType(query.contextType))
|
||||
&& (filter.expires_at === null
|
||||
|| Date.parse(filter.expires_at) > new Date().getTime());
|
||||
return (!query?.contextType || filter.context.includes(toServerSideType(query.contextType)))
|
||||
&& (filter.expires_at === null || Date.parse(filter.expires_at) > new Date().getTime());
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
@import 'components/react-toggle';
|
||||
@import 'components/video-player';
|
||||
@import 'components/audio-player';
|
||||
@import 'components/filters';
|
||||
@import 'components/crypto-donate';
|
||||
@import 'components/aliases';
|
||||
@import 'components/icon';
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
.filter-settings-panel {
|
||||
.fields-group .two-col {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
div.input {
|
||||
width: 45%;
|
||||
margin-right: 20px;
|
||||
|
||||
.label_input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 485px) {
|
||||
div.input {
|
||||
width: 100%;
|
||||
margin-right: 5px;
|
||||
|
||||
.label_input {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input.boolean {
|
||||
.label_input {
|
||||
@apply relative pl-7 text-black dark:text-white;
|
||||
|
||||
label {
|
||||
@apply text-sm;
|
||||
}
|
||||
|
||||
&__wrapper {
|
||||
@apply static;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hint {
|
||||
@apply block pl-7 text-xs text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
|
||||
.filter__container {
|
||||
@apply flex justify-between py-5 px-2 text-sm text-black dark:text-white;
|
||||
|
||||
.filter__phrase,
|
||||
.filter__contexts,
|
||||
.filter__details {
|
||||
@apply py-1;
|
||||
}
|
||||
|
||||
span.filter__list-label {
|
||||
@apply pr-1 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
|
||||
span.filter__list-value span {
|
||||
@apply pr-1 capitalize;
|
||||
|
||||
&::after {
|
||||
content: ',';
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
&::after {
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter__delete {
|
||||
@apply flex items-center h-5 m-2.5 cursor-pointer;
|
||||
|
||||
span.filter__delete-label {
|
||||
@apply text-gray-500 dark:text-gray-400 font-semibold;
|
||||
}
|
||||
|
||||
.filter__delete-icon {
|
||||
@apply mx-1 text-gray-500 dark:text-gray-400;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
[column-type='filled'] .status__wrapper,
|
||||
[column-type='filled'] .status-placeholder {
|
||||
@apply rounded-none shadow-none p-4;
|
||||
@apply bg-transparent dark:bg-transparent rounded-none shadow-none p-4;
|
||||
}
|
||||
|
||||
.status-check-box {
|
||||
|
|
|
@ -191,18 +191,6 @@ select {
|
|||
color: lighten($error-value-color, 12%);
|
||||
}
|
||||
|
||||
.fields-group {
|
||||
margin-bottom: 25px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input.radio_buttons .radio label {
|
||||
@apply text-gray-900;
|
||||
margin-bottom: 5px;
|
||||
|
|
Loading…
Reference in a new issue