Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
merge-requests/1524/head
marcin mikołajczak 2022-06-12 20:15:34 +02:00
rodzic 5fec879148
commit e78108efe2
14 zmienionych plików z 345 dodań i 329 usunięć

Wyświetl plik

@ -68,9 +68,9 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
const me = useAppSelector(state => state.me);
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.get<string | undefined>('accountId', undefined));
const accountId: string | undefined = useAppSelector(state => state.profile_hover_card.accountId || undefined);
const account = useAppSelector(state => accountId && getAccount(state, accountId));
const targetRef = useAppSelector(state => state.profile_hover_card.getIn(['ref', 'current']) as Element | null);
const targetRef = useAppSelector(state => state.profile_hover_card.ref?.current);
const badges = account ? getBadges(account) : [];
useEffect(() => {

Wyświetl plik

@ -1,15 +1,17 @@
// The output of this module is designed to mimic emoji-mart's
// "data" object, such that we can use it for a light version of emoji-mart's
// emojiIndex.search functionality.
const [ shortCodesToEmojiData, skins, categories, short_names ] = require('./emoji_compressed');
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
import emojiCompressed from './emoji_compressed';
import { unicodeToUnifiedName } from './unicode_to_unified_name';
const emojis = {};
const [ shortCodesToEmojiData, skins, categories, short_names ] = emojiCompressed;
const emojis: Record<string, any> = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
const [
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
_filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
searchData,
] = shortCodesToEmojiData[shortCode];
const [
@ -27,7 +29,14 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
};
});
module.exports = {
export {
emojis,
skins,
categories,
short_names,
};
export default {
emojis,
skins,
categories,

Wyświetl plik

@ -1,257 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
import snackbar from 'soapbox/actions/snackbar';
import Icon from 'soapbox/components/icon';
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';
const messages = defineMessages({
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
expires_hint: { id: 'column.filters.expires_hint', defaultMessage: 'Expiration dates are not currently supported' },
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
whole_word_header: { id: 'column.filters.whole_word_header', defaultMessage: 'Whole word' },
whole_word_hint: { id: 'column.filters.whole_word_hint', defaultMessage: 'When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word' },
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
subheading_filters: { id: 'column.filters.subheading_filters', defaultMessage: 'Current Filters' },
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
});
// const expirations = {
// null: 'Never',
// // 3600: '30 minutes',
// // 21600: '1 hour',
// // 43200: '12 hours',
// // 86400 : '1 day',
// // 604800: '1 week',
// };
const mapStateToProps = state => ({
filters: state.get('filters'),
});
export default @connect(mapStateToProps)
@injectIntl
class Filters extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
state = {
phrase: '',
expires_at: '',
home_timeline: true,
public_timeline: false,
notifications: false,
conversations: false,
irreversible: false,
whole_word: true,
}
componentDidMount() {
this.props.dispatch(fetchFilters());
}
handleInputChange = e => {
this.setState({ [e.target.name]: e.target.value });
}
handleSelectChange = e => {
this.setState({ [e.target.name]: e.target.value });
}
handleCheckboxChange = e => {
this.setState({ [e.target.name]: e.target.checked });
}
handleAddNew = e => {
e.preventDefault();
const { intl, dispatch } = this.props;
const { phrase, whole_word, expires_at, irreversible } = this.state;
const { home_timeline, public_timeline, notifications, conversations } = this.state;
const context = [];
if (home_timeline) {
context.push('home');
}
if (public_timeline) {
context.push('public');
}
if (notifications) {
context.push('notifications');
}
if (conversations) {
context.push('thread');
}
dispatch(createFilter(intl, phrase, expires_at, context, whole_word, irreversible)).then(response => {
return dispatch(fetchFilters());
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.create_error)));
});
}
handleFilterDelete = e => {
const { intl, dispatch } = this.props;
dispatch(deleteFilter(intl, e.currentTarget.dataset.value)).then(response => {
return dispatch(fetchFilters());
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.delete_error)));
});
}
render() {
const { intl, filters } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
return (
<Column className='filter-settings-panel' icon='filter' label={intl.formatMessage(messages.heading)}>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
</CardHeader>
<Form onSubmit={this.handleAddNew}>
<FormGroup labelText={intl.formatMessage(messages.keyword)}>
<Input
required
type='text'
name='phrase'
onChange={this.handleInputChange}
/>
</FormGroup>
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
<SelectDropdown
items={expirations}
defaultValue={expirations.never}
onChange={this.handleSelectChange}
/>
</FormGroup> */}
<FieldsGroup>
<Text tag='label'>
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
</Text>
<Text theme='muted' size='xs'>
<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)}
name='home_timeline'
checked={this.state.home_timeline}
onChange={this.handleCheckboxChange}
/>
<Checkbox
label={intl.formatMessage(messages.public_timeline)}
name='public_timeline'
checked={this.state.public_timeline}
onChange={this.handleCheckboxChange}
/>
<Checkbox
label={intl.formatMessage(messages.notifications)}
name='notifications'
checked={this.state.notifications}
onChange={this.handleCheckboxChange}
/>
<Checkbox
label={intl.formatMessage(messages.conversations)}
name='conversations'
checked={this.state.conversations}
onChange={this.handleCheckboxChange}
/>
</div>
</FieldsGroup>
<FieldsGroup>
<Checkbox
label={intl.formatMessage(messages.drop_header)}
hint={intl.formatMessage(messages.drop_hint)}
name='irreversible'
checked={this.state.irreversible}
onChange={this.handleCheckboxChange}
/>
<Checkbox
label={intl.formatMessage(messages.whole_word_header)}
hint={intl.formatMessage(messages.whole_word_hint)}
name='whole_word'
checked={this.state.whole_word}
onChange={this.handleCheckboxChange}
/>
</FieldsGroup>
<FormActions>
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
</FormActions>
</Form>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading_filters)} />
</CardHeader>
<ScrollableList
scrollKey='filters'
emptyMessage={emptyMessage}
>
{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.get('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.get('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'>
{filter.get('irreversible') ?
<span><FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /></span> :
<span><FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' /></span>
}
{filter.get('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={this.handleFilterDelete} data-value={filter.get('id')} aria-label={intl.formatMessage(messages.delete)}>
<Icon className='filter__delete-icon' id='times' size={40} />
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
</div>
</div>
))}
</ScrollableList>
</Column>
);
}
}

Wyświetl plik

@ -0,0 +1,229 @@
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchFilters, createFilter, deleteFilter } from 'soapbox/actions/filters';
import snackbar from 'soapbox/actions/snackbar';
import Icon from 'soapbox/components/icon';
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 { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
subheading_add_new: { id: 'column.filters.subheading_add_new', defaultMessage: 'Add New Filter' },
keyword: { id: 'column.filters.keyword', defaultMessage: 'Keyword or phrase' },
expires: { id: 'column.filters.expires', defaultMessage: 'Expire after' },
expires_hint: { id: 'column.filters.expires_hint', defaultMessage: 'Expiration dates are not currently supported' },
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
drop_header: { id: 'column.filters.drop_header', defaultMessage: 'Drop instead of hide' },
drop_hint: { id: 'column.filters.drop_hint', defaultMessage: 'Filtered posts will disappear irreversibly, even if filter is later removed' },
whole_word_header: { id: 'column.filters.whole_word_header', defaultMessage: 'Whole word' },
whole_word_hint: { id: 'column.filters.whole_word_hint', defaultMessage: 'When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word' },
add_new: { id: 'column.filters.add_new', defaultMessage: 'Add New Filter' },
create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' },
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
subheading_filters: { id: 'column.filters.subheading_filters', defaultMessage: 'Current Filters' },
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
});
// const expirations = {
// null: 'Never',
// // 3600: '30 minutes',
// // 21600: '1 hour',
// // 43200: '12 hours',
// // 86400 : '1 day',
// // 604800: '1 week',
// };
const Filters = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const filters = useAppSelector((state) => state.filters);
const [phrase, setPhrase] = useState('');
const [expiresAt] = useState('');
const [homeTimeline, setHomeTimeline] = useState(true);
const [publicTimeline, setPublicTimeline] = useState(false);
const [notifications, setNotifications] = useState(false);
const [conversations, setConversations] = useState(false);
const [irreversible, setIrreversible] = useState(false);
const [wholeWord, setWholeWord] = useState(true);
// const handleSelectChange = e => {
// this.setState({ [e.target.name]: e.target.value });
// };
const handleAddNew: React.FormEventHandler = e => {
e.preventDefault();
const context = [];
if (homeTimeline) {
context.push('home');
}
if (publicTimeline) {
context.push('public');
}
if (notifications) {
context.push('notifications');
}
if (conversations) {
context.push('thread');
}
dispatch(createFilter(intl, phrase, expiresAt, context, wholeWord, irreversible)).then(() => {
return dispatch(fetchFilters());
}).catch(error => {
dispatch(snackbar.error(intl.formatMessage(messages.create_error)));
});
};
const handleFilterDelete: React.MouseEventHandler<HTMLDivElement> = e => {
dispatch(deleteFilter(intl, e.currentTarget.dataset.value)).then(() => {
return dispatch(fetchFilters());
}).catch(() => {
dispatch(snackbar.error(intl.formatMessage(messages.delete_error)));
});
};
useEffect(() => {
dispatch(fetchFilters());
}, []);
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
return (
<Column className='filter-settings-panel' label={intl.formatMessage(messages.heading)}>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading_add_new)} />
</CardHeader>
<Form onSubmit={handleAddNew}>
<FormGroup labelText={intl.formatMessage(messages.keyword)}>
<Input
required
type='text'
name='phrase'
onChange={({ target }) => setPhrase(target.value)}
/>
</FormGroup>
{/* <FormGroup labelText={intl.formatMessage(messages.expires)} hintText={intl.formatMessage(messages.expires_hint)}>
<SelectDropdown
items={expirations}
defaultValue={expirations.never}
onChange={this.handleSelectChange}
/>
</FormGroup> */}
<FieldsGroup>
<Text tag='label'>
<FormattedMessage id='filters.context_header' defaultMessage='Filter contexts' />
</Text>
<Text theme='muted' size='xs'>
<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)}
name='home_timeline'
checked={homeTimeline}
onChange={({ target }) => setHomeTimeline(target.checked)}
/>
<Checkbox
label={intl.formatMessage(messages.public_timeline)}
name='public_timeline'
checked={publicTimeline}
onChange={({ target }) => setPublicTimeline(target.checked)}
/>
<Checkbox
label={intl.formatMessage(messages.notifications)}
name='notifications'
checked={notifications}
onChange={({ target }) => setNotifications(target.checked)}
/>
<Checkbox
label={intl.formatMessage(messages.conversations)}
name='conversations'
checked={conversations}
onChange={({ target }) => setConversations(target.checked)}
/>
</div>
</FieldsGroup>
<FieldsGroup>
<Checkbox
label={intl.formatMessage(messages.drop_header)}
hint={intl.formatMessage(messages.drop_hint)}
name='irreversible'
checked={irreversible}
onChange={({ target }) => setIrreversible(target.checked)}
/>
<Checkbox
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>
<FormActions>
<Button type='submit' theme='primary'>{intl.formatMessage(messages.add_new)}</Button>
</FormActions>
</Form>
<CardHeader>
<CardTitle title={intl.formatMessage(messages.subheading_filters)} />
</CardHeader>
<ScrollableList
scrollKey='filters'
emptyMessage={emptyMessage}
>
{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'>
{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' id='times' size={40} />
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
</div>
</div>
))}
</ScrollableList>
</Column>
);
};
export default Filters;

Wyświetl plik

@ -0,0 +1,22 @@
/**
* Filter normalizer:
* Converts API filters into our internal format.
* @see {@link https://docs.joinmastodon.org/entities/filter/}
*/
import { List as ImmutableList, Map as ImmutableMap, Record as ImmutableRecord, fromJS } from 'immutable';
// https://docs.joinmastodon.org/entities/filter/
export const FilterRecord = ImmutableRecord({
id: '',
phrase: '',
context: ImmutableList<string>(),
whole_word: false,
expires_at: '',
irreversible: false,
});
export const normalizeFilter = (filter: Record<string, any>) => {
return FilterRecord(
ImmutableMap(fromJS(filter)),
);
};

Wyświetl plik

@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card';
export { ChatRecord, normalizeChat } from './chat';
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
export { EmojiRecord, normalizeEmoji } from './emoji';
export { FilterRecord, normalizeFilter } from './filter';
export { HistoryRecord, normalizeHistory } from './history';
export { InstanceRecord, normalizeInstance } from './instance';
export { ListRecord, normalizeList } from './list';

Wyświetl plik

@ -1,4 +1,4 @@
import { List as ImmutableList, fromJS } from 'immutable';
import { List as ImmutableList, Map as ImmutableMap, fromJS } from 'immutable';
import { emojis as emojiData } from 'soapbox/features/emoji/emoji_mart_data_light';
import { addCustomToPool } from 'soapbox/features/emoji/emoji_mart_search_light';
@ -6,15 +6,18 @@ import { addCustomToPool } from 'soapbox/features/emoji/emoji_mart_search_light'
import { CUSTOM_EMOJIS_FETCH_SUCCESS } from '../actions/custom_emojis';
import { buildCustomEmojis } from '../features/emoji/emoji';
import type { AnyAction } from 'redux';
import type { APIEntity } from 'soapbox/types/entities';
const initialState = ImmutableList();
// Populate custom emojis for composer autosuggest
const autosuggestPopulate = emojis => {
const autosuggestPopulate = (emojis: ImmutableList<ImmutableMap<string, string>>) => {
addCustomToPool(buildCustomEmojis(emojis));
};
const importEmojis = (state, customEmojis) => {
const emojis = fromJS(customEmojis).filter(emoji => {
const importEmojis = (customEmojis: APIEntity[]) => {
const emojis = (fromJS(customEmojis) as ImmutableList<ImmutableMap<string, string>>).filter((emoji) => {
// If a custom emoji has the shortcode of a Unicode emoji, skip it.
// Otherwise it breaks EmojiMart.
// https://gitlab.com/soapbox-pub/soapbox-fe/-/issues/610
@ -26,9 +29,9 @@ const importEmojis = (state, customEmojis) => {
return emojis;
};
export default function custom_emojis(state = initialState, action) {
export default function custom_emojis(state = initialState, action: AnyAction) {
if (action.type === CUSTOM_EMOJIS_FETCH_SUCCESS) {
return importEmojis(state, action.custom_emojis);
return importEmojis(action.custom_emojis);
}
return state;

Wyświetl plik

@ -0,0 +1,23 @@
import { List as ImmutableList } from 'immutable';
import { normalizeFilter } from 'soapbox/normalizers';
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
import type { AnyAction } from 'redux';
import type { APIEntity, Filter as FilterEntity } from 'soapbox/types/entities';
type State = ImmutableList<FilterEntity>;
const importFilters = (_state: State, filters: APIEntity[]): State => {
return ImmutableList(filters.map((filter) => normalizeFilter(filter)));
};
export default function filters(state: State = ImmutableList<FilterEntity>(), action: AnyAction): State {
switch (action.type) {
case FILTERS_FETCH_SUCCESS:
return importFilters(state, action.filters);
default:
return state;
}
}

Wyświetl plik

@ -1,25 +0,0 @@
import {
Map as ImmutableMap,
List as ImmutableList,
fromJS,
} from 'immutable';
import { FILTERS_FETCH_SUCCESS } from '../actions/filters';
import type { AnyAction } from 'redux';
type Filter = ImmutableMap<string, any>;
type State = ImmutableList<Filter>;
const importFilters = (_state: State, filters: unknown): State => {
return ImmutableList(fromJS(filters)).map(filter => ImmutableMap(fromJS(filter)));
};
export default function filters(state: State = ImmutableList<Filter>(), action: AnyAction): State {
switch (action.type) {
case FILTERS_FETCH_SUCCESS:
return importFilters(state, action.filters);
default:
return state;
}
}

Wyświetl plik

@ -1,28 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import {
PROFILE_HOVER_CARD_OPEN,
PROFILE_HOVER_CARD_CLOSE,
PROFILE_HOVER_CARD_UPDATE,
} from 'soapbox/actions/profile_hover_card';
const initialState = ImmutableMap();
export default function profileHoverCard(state = initialState, action) {
switch (action.type) {
case PROFILE_HOVER_CARD_OPEN:
return ImmutableMap({
ref: action.ref,
accountId: action.accountId,
});
case PROFILE_HOVER_CARD_UPDATE:
return state.set('hovered', true);
case PROFILE_HOVER_CARD_CLOSE:
if (state.get('hovered') === true && !action.force)
return state;
else
return ImmutableMap();
default:
return state;
}
}

Wyświetl plik

@ -0,0 +1,36 @@
import { Record as ImmutableRecord } from 'immutable';
import {
PROFILE_HOVER_CARD_OPEN,
PROFILE_HOVER_CARD_CLOSE,
PROFILE_HOVER_CARD_UPDATE,
} from 'soapbox/actions/profile_hover_card';
import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({
ref: null as React.MutableRefObject<HTMLDivElement> | null,
accountId: '',
hovered: false,
});
type State = ReturnType<typeof ReducerRecord>;
export default function profileHoverCard(state: State = ReducerRecord(), action: AnyAction) {
switch (action.type) {
case PROFILE_HOVER_CARD_OPEN:
return state.withMutations((state) => {
state.set('ref', action.ref);
state.set('accountId', action.accountId);
});
case PROFILE_HOVER_CARD_UPDATE:
return state.set('hovered', true);
case PROFILE_HOVER_CARD_CLOSE:
if (state.get('hovered') === true && !action.force)
return state;
else
return ReducerRecord();
default:
return state;
}
}

Wyświetl plik

@ -14,7 +14,7 @@ import { shouldFilter } from 'soapbox/utils/timelines';
import type { ReducerChat } from 'soapbox/reducers/chats';
import type { RootState } from 'soapbox/store';
import type { Notification } from 'soapbox/types/entities';
import type { Filter as FilterEntity, Notification } from 'soapbox/types/entities';
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
@ -104,18 +104,18 @@ const toServerSideType = (columnType: string): string => {
type FilterContext = { contextType?: string };
export const getFilters = (state: RootState, query: FilterContext) => {
return state.filters.filter((filter): boolean => {
return state.filters.filter((filter) => {
return query?.contextType
&& filter.get('context').includes(toServerSideType(query.contextType))
&& (filter.get('expires_at') === null
|| Date.parse(filter.get('expires_at')) > new Date().getTime());
&& filter.context.includes(toServerSideType(query.contextType))
&& (filter.expires_at === null
|| Date.parse(filter.expires_at) > new Date().getTime());
});
};
const escapeRegExp = (string: string) =>
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
export const regexFromFilters = (filters: ImmutableList<ImmutableMap<string, any>>) => {
export const regexFromFilters = (filters: ImmutableList<FilterEntity>) => {
if (filters.size === 0) return null;
return new RegExp(filters.map(filter => {

Wyświetl plik

@ -8,6 +8,7 @@ import {
ChatMessageRecord,
EmojiRecord,
FieldRecord,
FilterRecord,
HistoryRecord,
InstanceRecord,
ListRecord,
@ -31,6 +32,7 @@ type Chat = ReturnType<typeof ChatRecord>;
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
type Emoji = ReturnType<typeof EmojiRecord>;
type Field = ReturnType<typeof FieldRecord>;
type Filter = ReturnType<typeof FilterRecord>;
type History = ReturnType<typeof HistoryRecord>;
type Instance = ReturnType<typeof InstanceRecord>;
type List = ReturnType<typeof ListRecord>;
@ -68,6 +70,7 @@ export {
ChatMessage,
Emoji,
Field,
Filter,
History,
Instance,
List,