TypeScript
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
5fec879148
commit
e78108efe2
14 changed files with 322 additions and 33 deletions
|
@ -68,9 +68,9 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
||||||
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
|
||||||
|
|
||||||
const me = useAppSelector(state => state.me);
|
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 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) : [];
|
const badges = account ? getBadges(account) : [];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
Binary file not shown.
Binary file not shown.
229
app/soapbox/features/filters/index.tsx
Normal file
229
app/soapbox/features/filters/index.tsx
Normal 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;
|
22
app/soapbox/normalizers/filter.ts
Normal file
22
app/soapbox/normalizers/filter.ts
Normal 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)),
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,6 +6,7 @@ export { CardRecord, normalizeCard } from './card';
|
||||||
export { ChatRecord, normalizeChat } from './chat';
|
export { ChatRecord, normalizeChat } from './chat';
|
||||||
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
|
export { ChatMessageRecord, normalizeChatMessage } from './chat_message';
|
||||||
export { EmojiRecord, normalizeEmoji } from './emoji';
|
export { EmojiRecord, normalizeEmoji } from './emoji';
|
||||||
|
export { FilterRecord, normalizeFilter } from './filter';
|
||||||
export { HistoryRecord, normalizeHistory } from './history';
|
export { HistoryRecord, normalizeHistory } from './history';
|
||||||
export { InstanceRecord, normalizeInstance } from './instance';
|
export { InstanceRecord, normalizeInstance } from './instance';
|
||||||
export { ListRecord, normalizeList } from './list';
|
export { ListRecord, normalizeList } from './list';
|
||||||
|
|
Binary file not shown.
23
app/soapbox/reducers/filters.ts
Normal file
23
app/soapbox/reducers/filters.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
36
app/soapbox/reducers/profile_hover_card.ts
Normal file
36
app/soapbox/reducers/profile_hover_card.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ import { shouldFilter } from 'soapbox/utils/timelines';
|
||||||
|
|
||||||
import type { ReducerChat } from 'soapbox/reducers/chats';
|
import type { ReducerChat } from 'soapbox/reducers/chats';
|
||||||
import type { RootState } from 'soapbox/store';
|
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 : '';
|
const normalizeId = (id: any): string => typeof id === 'string' ? id : '';
|
||||||
|
|
||||||
|
@ -104,18 +104,18 @@ const toServerSideType = (columnType: string): string => {
|
||||||
type FilterContext = { contextType?: string };
|
type FilterContext = { contextType?: string };
|
||||||
|
|
||||||
export const getFilters = (state: RootState, query: FilterContext) => {
|
export const getFilters = (state: RootState, query: FilterContext) => {
|
||||||
return state.filters.filter((filter): boolean => {
|
return state.filters.filter((filter) => {
|
||||||
return query?.contextType
|
return query?.contextType
|
||||||
&& filter.get('context').includes(toServerSideType(query.contextType))
|
&& filter.context.includes(toServerSideType(query.contextType))
|
||||||
&& (filter.get('expires_at') === null
|
&& (filter.expires_at === null
|
||||||
|| Date.parse(filter.get('expires_at')) > new Date().getTime());
|
|| Date.parse(filter.expires_at) > new Date().getTime());
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const escapeRegExp = (string: string) =>
|
const escapeRegExp = (string: string) =>
|
||||||
string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched 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;
|
if (filters.size === 0) return null;
|
||||||
|
|
||||||
return new RegExp(filters.map(filter => {
|
return new RegExp(filters.map(filter => {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ChatMessageRecord,
|
ChatMessageRecord,
|
||||||
EmojiRecord,
|
EmojiRecord,
|
||||||
FieldRecord,
|
FieldRecord,
|
||||||
|
FilterRecord,
|
||||||
HistoryRecord,
|
HistoryRecord,
|
||||||
InstanceRecord,
|
InstanceRecord,
|
||||||
ListRecord,
|
ListRecord,
|
||||||
|
@ -31,6 +32,7 @@ type Chat = ReturnType<typeof ChatRecord>;
|
||||||
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
type ChatMessage = ReturnType<typeof ChatMessageRecord>;
|
||||||
type Emoji = ReturnType<typeof EmojiRecord>;
|
type Emoji = ReturnType<typeof EmojiRecord>;
|
||||||
type Field = ReturnType<typeof FieldRecord>;
|
type Field = ReturnType<typeof FieldRecord>;
|
||||||
|
type Filter = ReturnType<typeof FilterRecord>;
|
||||||
type History = ReturnType<typeof HistoryRecord>;
|
type History = ReturnType<typeof HistoryRecord>;
|
||||||
type Instance = ReturnType<typeof InstanceRecord>;
|
type Instance = ReturnType<typeof InstanceRecord>;
|
||||||
type List = ReturnType<typeof ListRecord>;
|
type List = ReturnType<typeof ListRecord>;
|
||||||
|
@ -68,6 +70,7 @@ export {
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
Emoji,
|
Emoji,
|
||||||
Field,
|
Field,
|
||||||
|
Filter,
|
||||||
History,
|
History,
|
||||||
Instance,
|
Instance,
|
||||||
List,
|
List,
|
||||||
|
|
Loading…
Reference in a new issue