Merge branch 'ts' into 'develop'

TypeScript, React.FC

See merge request soapbox-pub/soapbox-fe!1524
This commit is contained in:
marcin mikołajczak 2022-06-12 22:35:57 +00:00
commit 7ff991f8b3
103 changed files with 654 additions and 43 deletions

View file

@ -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(() => {

View file

@ -27,7 +27,7 @@ interface IModal {
/** Callback when the modal is cancelled. */
cancelAction?: () => void,
/** Cancel button text. */
cancelText?: string,
cancelText?: React.ReactNode,
/** URL to an SVG icon for the close button. */
closeIcon?: string,
/** Position of the close button. */

View file

@ -9,13 +9,13 @@ import type { Status } from 'soapbox/types/entities';
interface IReplyIndicator {
status?: Status,
onCancel: () => void,
onCancel?: () => void,
hideActions: boolean,
}
const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCancel }) => {
const handleClick = () => {
onCancel();
onCancel!();
};
if (!status) {
@ -23,7 +23,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCanc
}
let actions = {};
if (!hideActions) {
if (!hideActions && onCancel) {
actions = {
onActionClick: handleClick,
actionIcon: require('@tabler/icons/icons/x.svg'),

View file

@ -10,7 +10,7 @@ import { getTitle } from '../utils/coin_db';
import CryptoIcon from './crypto_icon';
interface ICryptoAddress {
export interface ICryptoAddress {
address: string,
ticker: string,
note?: string,

View file

@ -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;

View file

@ -13,7 +13,7 @@ import { buildStatus } from '../builder';
import ScheduledStatusActionBar from './scheduled_status_action_bar';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
import type { Account as AccountEntity, Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
interface IScheduledStatus {
statusId: string,
@ -55,7 +55,7 @@ const ScheduledStatus: React.FC<IScheduledStatus> = ({ statusId, ...other }) =>
/>
)}
{status.poll && <PollPreview poll={status.poll} />}
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
<ScheduledStatusActionBar status={status} {...other} />
</div>

View file

@ -0,0 +1,48 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
import { Modal, Stack, Text } from 'soapbox/components/ui';
import ReplyIndicator from 'soapbox/features/compose/components/reply_indicator';
import type { Status as StatusEntity } from 'soapbox/types/entities';
const messages = defineMessages({
cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
reblog: { id: 'status.reblog', defaultMessage: 'Repost' },
});
interface IBoostModal {
status: StatusEntity,
onReblog: (status: StatusEntity) => void,
onClose: () => void,
}
const BoostModal: React.FC<IBoostModal> = ({ status, onReblog, onClose }) => {
const intl = useIntl();
const handleReblog = () => {
onReblog(status);
onClose();
};
const buttonText = status.reblogged ? messages.cancel_reblog : messages.reblog;
return (
<Modal
title='Repost?'
confirmationAction={handleReblog}
confirmationText={intl.formatMessage(buttonText)}
>
<Stack space={4}>
<ReplyIndicator status={status} hideActions />
<Text>
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
</Text>
</Stack>
</Modal>
);
};
export default BoostModal;

View file

@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal } from 'soapbox/components/ui';
import { SimpleForm, FieldsGroup, Checkbox } from 'soapbox/features/forms';
interface IConfirmationModal {
heading: React.ReactNode,
message: React.ReactNode,
confirm: React.ReactNode,
onClose: (type: string) => void,
onConfirm: () => void,
secondary: React.ReactNode,
onSecondary?: () => void,
onCancel: () => void,
checkbox?: JSX.Element,
}
const ConfirmationModal: React.FC<IConfirmationModal> = ({
heading,
message,
confirm,
onClose,
onConfirm,
secondary,
onSecondary,
onCancel,
checkbox,
}) => {
const [checked, setChecked] = useState(false);
const handleClick = () => {
onClose('CONFIRM');
onConfirm();
};
const handleSecondary = () => {
onClose('CONFIRM');
onSecondary!();
};
const handleCancel = () => {
onClose('CONFIRM');
if (onCancel) onCancel();
};
const handleCheckboxChange: React.ChangeEventHandler<HTMLInputElement> = e => {
setChecked(e.target.checked);
};
return (
<Modal
title={heading}
confirmationAction={handleClick}
confirmationText={confirm}
confirmationDisabled={checkbox && !checked}
confirmationTheme='danger'
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
cancelAction={handleCancel}
secondaryText={secondary}
secondaryAction={onSecondary && handleSecondary}
>
<p className='text-gray-600 dark:text-gray-300'>{message}</p>
<div className='mt-2'>
{checkbox && <div className='confirmation-modal__checkbox'>
<SimpleForm>
<FieldsGroup>
<Checkbox
onChange={handleCheckboxChange}
label={checkbox}
checked={checked}
/>
</FieldsGroup>
</SimpleForm>
</div>}
</div>
</Modal>
);
};
export default ConfirmationModal;

View file

@ -0,0 +1,16 @@
import React from 'react';
import DetailedCryptoAddress from 'soapbox/features/crypto_donate/components/detailed_crypto_address';
import type { ICryptoAddress } from '../../crypto_donate/components/crypto_address';
const CryptoDonateModal: React.FC<ICryptoAddress> = (props) => {
return (
<div className='modal-root__modal crypto-donate-modal'>
<DetailedCryptoAddress {...props} />
</div>
);
};
export default CryptoDonateModal;

View file

@ -0,0 +1,45 @@
import React, { useEffect } from 'react';
import { NavLink } from 'react-router-dom';
import { createSelector } from 'reselect';
import { fetchLists } from 'soapbox/actions/lists';
import Icon from 'soapbox/components/icon';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import type { List as ImmutableList } from 'immutable';
import type { RootState } from 'soapbox/store';
import type { List as ListEntity } from 'soapbox/types/entities';
const getOrderedLists = createSelector([(state: RootState) => state.lists], lists => {
if (!lists) {
return lists;
}
return lists.toList().filter(item => !!item).sort((a, b) => (a as ListEntity).title.localeCompare((b as ListEntity).title)).take(4) as ImmutableList<ListEntity>;
});
const ListPanel = () => {
const dispatch = useAppDispatch();
const lists = useAppSelector((state) => getOrderedLists(state));
useEffect(() => {
dispatch(fetchLists());
}, []);
if (!lists || lists.isEmpty()) {
return null;
}
return (
<div>
<hr />
{lists.map(list => (
<NavLink key={list.id} className='column-link column-link--transparent' strict to={`/list/${list.id}`}><Icon className='column-link__icon' id='list-ul' fixedWidth />{list.title}</NavLink>
))}
</div>
);
};
export default ListPanel;

View file

@ -0,0 +1,77 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import Toggle from 'react-toggle';
import { muteAccount } from 'soapbox/actions/accounts';
import { closeModal } from 'soapbox/actions/modals';
import { toggleHideNotifications } from 'soapbox/actions/mutes';
import { Modal, HStack, Stack, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
const getAccount = makeGetAccount();
const MuteModal = () => {
const dispatch = useAppDispatch();
const account = useAppSelector((state) => getAccount(state, state.mutes.new.accountId!));
const notifications = useAppSelector((state) => state.mutes.new.notifications);
if (!account) return null;
const handleClick = () => {
dispatch(closeModal());
dispatch(muteAccount(account.id, notifications));
};
const handleCancel = () => {
dispatch(closeModal());
};
const toggleNotifications = () => {
dispatch(toggleHideNotifications());
};
return (
<Modal
title={
<FormattedMessage
id='confirmations.mute.heading'
defaultMessage='Mute @{name}'
values={{ name: account.acct }}
/>
}
onClose={handleCancel}
confirmationAction={handleClick}
confirmationText={<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />}
cancelText={<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />}
cancelAction={handleCancel}
>
<Stack space={4}>
<Text>
<FormattedMessage
id='confirmations.mute.message'
defaultMessage='Are you sure you want to mute {name}?'
values={{ name: <strong>@{account.acct}</strong> }}
/>
</Text>
<label>
<HStack alignItems='center' space={2}>
<Text tag='span'>
<FormattedMessage id='mute_modal.hide_notifications' defaultMessage='Hide notifications from this user?' />
</Text>
<Toggle
checked={notifications}
onChange={toggleNotifications}
icons={false}
/>
</HStack>
</label>
</Stack>
</Modal>
);
};
export default MuteModal;

View file

@ -14,7 +14,7 @@ import { buildStatus } from '../util/pending_status_builder';
import PollPreview from './poll_preview';
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
import type { Account as AccountEntity, Poll as PollEntity, Status as StatusEntity } from 'soapbox/types/entities';
const shouldHaveCard = (pendingStatus: StatusEntity) => {
return Boolean(pendingStatus.content.match(/https?:\/\/\S*/));
@ -81,7 +81,7 @@ const PendingStatus: React.FC<IPendingStatus> = ({ idempotencyKey, className, mu
<PendingStatusMedia status={status} />
{status.poll && <PollPreview poll={status.poll} />}
{status.poll && <PollPreview poll={status.poll as PollEntity} />}
{status.quote && <QuotedStatus statusId={status.quote as string} />}
</div>

View file

@ -0,0 +1,44 @@
import classNames from 'classnames';
import React from 'react';
import { Poll as PollEntity, PollOption as PollOptionEntity } from 'soapbox/types/entities';
interface IPollPreview {
poll: PollEntity,
}
const PollPreview: React.FC<IPollPreview> = ({ poll }) => {
const renderOption = (option: PollOptionEntity, index: number) => {
const showResults = poll.voted || poll.expired;
return (
<li key={index}>
<label className={classNames('poll__text', { selectable: !showResults })}>
<input
name='vote-options'
type={poll.multiple ? 'checkbox' : 'radio'}
disabled
/>
<span className={classNames('poll__input', { checkbox: poll.multiple })} />
<span dangerouslySetInnerHTML={{ __html: option.title_emojified }} />
</label>
</li>
);
};
if (!poll) {
return null;
}
return (
<div className='poll'>
<ul>
{poll.options.map((option, i) => renderOption(option, i))}
</ul>
</div>
);
};
export default PollPreview;

View file

@ -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)),
);
};

View file

@ -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';

View file

@ -47,8 +47,18 @@ const normalizeEmojis = (entity: ImmutableMap<string, any>) => {
});
};
const normalizePollOption = (option: ImmutableMap<string, any>, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
const normalizePollOption = (option: ImmutableMap<string, any> | string, emojis: ImmutableList<ImmutableMap<string, string>> = ImmutableList()) => {
const emojiMap = makeEmojiMap(emojis);
if (typeof option === 'string') {
const titleEmojified = emojify(escapeTextContentForBrowser(option), emojiMap);
return PollOptionRecord({
title: option,
title_emojified: titleEmojified,
});
}
const titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap);
return PollOptionRecord(

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 => {

View file

@ -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,

Some files were not shown because too many files have changed in this diff Show more