Merge, store Lexical editorState

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2023-03-02 19:42:31 +01:00
777 changed files with 25645 additions and 29568 deletions

View File

@ -5,7 +5,7 @@ import { useAppSelector } from 'soapbox/hooks';
import { makeGetAccount } from 'soapbox/selectors';
interface IAutosuggestAccount {
id: string,
id: string
}
const AutosuggestAccount: React.FC<IAutosuggestAccount> = ({ id }) => {

View File

@ -1,14 +1,14 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React from 'react';
import { IconButton } from 'soapbox/components/ui';
interface IComposeFormButton {
icon: string,
title?: string,
active?: boolean,
disabled?: boolean,
onClick: () => void,
icon: string
title?: string
active?: boolean
disabled?: boolean
onClick: () => void
}
const ComposeFormButton: React.FC<IComposeFormButton> = ({
@ -22,7 +22,7 @@ const ComposeFormButton: React.FC<IComposeFormButton> = ({
<div>
<IconButton
className={
classNames({
clsx({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': !active,
'text-primary-500 hover:text-primary-600 dark:text-primary-500 dark:hover:text-primary-400': active,
})

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { defineMessages, FormattedMessage, MessageDescriptor, useIntl } from 'react-intl';
import { Link, useHistory } from 'react-router-dom';
@ -15,7 +15,6 @@ import {
} from 'soapbox/actions/compose';
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
import Icon from 'soapbox/components/icon';
import { Button, HStack, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
import { isMobile } from 'soapbox/is-mobile';
@ -58,14 +57,15 @@ const messages = defineMessages({
});
interface IComposeForm<ID extends string> {
id: ID extends 'default' ? never : ID,
shouldCondense?: boolean,
autoFocus?: boolean,
clickableAreaRef?: React.RefObject<HTMLDivElement>,
event?: string,
id: ID extends 'default' ? never : ID
shouldCondense?: boolean
autoFocus?: boolean
clickableAreaRef?: React.RefObject<HTMLDivElement>
event?: string
group?: string
}
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event }: IComposeForm<ID>) => {
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group }: IComposeForm<ID>) => {
const history = useHistory();
const intl = useIntl();
const dispatch = useAppDispatch();
@ -75,10 +75,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const showSearch = useAppSelector((state) => state.search.submitted && !state.search.hidden);
const isModalOpen = useAppSelector((state) => !!(state.modals.size && state.modals.last()!.modalType === 'COMPOSE'));
const maxTootChars = configuration.getIn(['statuses', 'max_characters']) as number;
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
const scheduledStatusCount = useAppSelector((state) => state.scheduled_statuses.size);
const features = useFeatures();
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose;
const prevSpoiler = usePrevious(spoiler);
const hasPoll = !!compose.poll;
@ -230,7 +230,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{features.media && <UploadButtonContainer composeId={id} />}
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
{features.polls && <PollButton composeId={id} />}
{features.privacyScopes && <PrivacyDropdown composeId={id} />}
{features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
{features.spoilers && <SpoilerButton composeId={id} />}
</HStack>
@ -272,7 +272,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
return (
<Stack className='w-full' space={4} ref={formRef} onClick={handleClick} element='form' onSubmit={handleSubmit}>
{scheduledStatusCount > 0 && !event && (
{scheduledStatusCount > 0 && !event && !group && (
<Warning
message={(
<FormattedMessage
@ -293,18 +293,19 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<WarningContainer composeId={id} />
{!shouldCondense && !event && <ReplyIndicatorContainer composeId={id} />}
{!shouldCondense && !event && !group && <ReplyIndicatorContainer composeId={id} />}
{!shouldCondense && !event && <ReplyMentions composeId={id} />}
{!shouldCondense && !event && !group && <ReplyMentions composeId={id} />}
<div>
<ComposeEditor
ref={editorStateRef}
condensed={condensed}
onFocus={handleComposeFocus}
autoFocus={shouldAutoFocus}
composeId={id}
/>
{
!condensed &&
{!condensed && (
<Stack space={4} className='compose-form__modifiers'>
<UploadForm composeId={id} />
<PollForm composeId={id} />
@ -318,7 +319,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
ref={spoilerTextRef}
/>
</Stack>
}
)}
</div>
<AutosuggestTextarea
@ -337,12 +338,14 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
autoFocus={shouldAutoFocus}
condensed={condensed}
id='compose-textarea'
/>
>
<></>
</AutosuggestTextarea>
<QuotedStatusContainer composeId={id} />
<div
className={classNames('flex flex-wrap items-center justify-between', {
className={clsx('flex flex-wrap items-center justify-between', {
'hidden': condensed,
})}
>
@ -358,8 +361,6 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
<Button type='submit' theme='primary' icon={publishIcon} text={publishText} disabled={disabledButton} />
</HStack>
{/* <HStack alignItems='center' space={4}>
</HStack> */}
</div>
</Stack>
);

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import React, { useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -92,8 +92,8 @@ const messages = defineMessages({
});
interface IEmojiPickerDropdown {
onPickEmoji: (data: EmojiType) => void,
button?: JSX.Element,
onPickEmoji: (data: EmojiType) => void
button?: JSX.Element
}
const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ onPickEmoji, button }) => {
@ -180,7 +180,7 @@ const EmojiPickerDropdown: React.FC<IEmojiPickerDropdown> = ({ onPickEmoji, butt
tabIndex={0}
>
{button || <IconButton
className={classNames({
className={clsx({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
'pulse-loading': active && loading,
})}

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import React, { useCallback, useEffect, useRef, useState } from 'react';
@ -32,14 +32,14 @@ const messages = defineMessages({
});
interface IEmojiPickerMenu {
customEmojis: ImmutableList<ImmutableMap<string, string>>,
loading?: boolean,
onClose: () => void,
onPick: (emoji: Emoji) => void,
onSkinTone: (skinTone: number) => void,
skinTone?: number,
frequentlyUsedEmojis?: Array<string>,
style?: React.CSSProperties,
customEmojis: ImmutableList<ImmutableMap<string, string>>
loading?: boolean
onClose: () => void
onPick: (emoji: Emoji) => void
onSkinTone: (skinTone: number) => void
skinTone?: number
frequentlyUsedEmojis?: Array<string>
style?: React.CSSProperties
}
const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
@ -72,8 +72,8 @@ const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(customEmojis) as Set<string>).sort());
const handleDocumentClick = useCallback(e => {
if (node.current && !node.current.contains(e.target)) {
const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
if (node.current && !node.current.contains(e.target as Node)) {
onClose();
}
}, []);
@ -136,7 +136,7 @@ const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
const title = intl.formatMessage(messages.emoji);
return (
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={node}>
<div className={clsx('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={node}>
<EmojiPicker
perLine={8}
emojiSize={22}

View File

@ -7,9 +7,9 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
interface IModifierPickerMenu {
active: boolean,
onSelect: (modifier: number) => void,
onClose: () => void,
active: boolean
onSelect: (modifier: number) => void
onClose: () => void
}
const ModifierPickerMenu: React.FC<IModifierPickerMenu> = ({ active, onSelect, onClose }) => {
@ -19,8 +19,8 @@ const ModifierPickerMenu: React.FC<IModifierPickerMenu> = ({ active, onSelect, o
onSelect(+e.currentTarget.getAttribute('data-index')! * 1);
};
const handleDocumentClick = useCallback((e => {
if (node.current && !node.current.contains(e.target)) {
const handleDocumentClick = useCallback(((e: MouseEvent | TouchEvent) => {
if (node.current && !node.current.contains(e.target as Node)) {
onClose();
}
}), []);

View File

@ -6,11 +6,11 @@ import ModifierPickerMenu from './modifier-picker-menu';
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
interface IModifierPicker {
active: boolean,
modifier?: number,
onOpen: () => void,
onClose: () => void,
onChange: (skinTone: number) => void,
active: boolean
modifier?: number
onOpen: () => void
onClose: () => void
onChange: (skinTone: number) => void
}
const ModifierPicker: React.FC<IModifierPicker> = ({ active, modifier, onOpen, onClose, onChange }) => {

View File

@ -13,7 +13,7 @@ const messages = defineMessages({
interface IPollButton {
composeId: string
disabled?: boolean,
disabled?: boolean
}
const PollButton: React.FC<IPollButton> = ({ composeId, disabled }) => {

View File

@ -42,7 +42,7 @@ const DurationSelector = ({ onDurationChange }: IDurationSelector) => {
}, [value]);
return (
<div className='grid grid-cols-1 gap-y-2 gap-x-2 sm:grid-cols-3'>
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
<div className='sm:col-span-1'>
<Select
value={days}

View File

@ -79,7 +79,7 @@ const Option: React.FC<IOption> = ({
</div>
<AutosuggestInput
className='rounded-md dark:!bg-transparent !bg-transparent'
className='rounded-md !bg-transparent dark:!bg-transparent'
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
maxLength={maxChars}
value={title}
@ -105,7 +105,7 @@ const Option: React.FC<IOption> = ({
};
interface IPollForm {
composeId: string,
composeId: string
}
const PollForm: React.FC<IPollForm> = ({ composeId }) => {

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import { supportsPassiveEvents } from 'detect-passive-events';
import React, { useState, useRef, useEffect } from 'react';
import { useIntl, defineMessages } from 'react-intl';
@ -30,13 +30,13 @@ const messages = defineMessages({
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
interface IPrivacyDropdownMenu {
style?: React.CSSProperties,
items: any[],
value: string,
placement: string,
onClose: () => void,
onChange: (value: string | null) => void,
unavailable?: boolean,
style?: React.CSSProperties
items: any[]
value: string
placement: string
onClose: () => void
onChange: (value: string | null) => void
unavailable?: boolean
}
const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, placement, value, onClose, onChange }) => {
@ -120,9 +120,9 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
// It should not be transformed when mounting because the resulting
// size will be used to determine the coordinate of the menu by
// react-overlays
<div className={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }} role='listbox' ref={node}>
<div className={clsx('privacy-dropdown__dropdown', placement)} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : undefined }} role='listbox' ref={node}>
{items.map(item => (
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={handleKeyDown} onClick={handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? focusedItem : null}>
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={handleKeyDown} onClick={handleClick} className={clsx('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? focusedItem : null}>
<div className='privacy-dropdown__option__icon'>
<Icon src={item.icon} />
</div>
@ -140,7 +140,7 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({ style, items, pla
};
interface IPrivacyDropdown {
composeId: string,
composeId: string
}
const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
@ -239,10 +239,13 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
const valueOption = options.find(item => item.value === value);
return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={handleKeyDown} ref={node}>
<div className={classNames('privacy-dropdown__value', { active: valueOption && options.indexOf(valueOption) === 0 })}>
<div className={clsx('privacy-dropdown', placement, { active: open })} onKeyDown={handleKeyDown} ref={node}>
<div className={clsx('privacy-dropdown__value', { active: valueOption && options.indexOf(valueOption) === 0 })}>
<IconButton
className='text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
className={clsx({
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': !open,
'text-primary-500 hover:text-primary-600 dark:text-primary-500 dark:hover:text-primary-400': open,
})}
src={valueOption?.icon}
title={intl.formatMessage(messages.change_privacy)}
onClick={handleToggle}

View File

@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
@ -8,12 +9,13 @@ import { isRtl } from 'soapbox/rtl';
import type { Status } from 'soapbox/types/entities';
interface IReplyIndicator {
status?: Status,
onCancel?: () => void,
hideActions: boolean,
className?: string
status?: Status
onCancel?: () => void
hideActions: boolean
}
const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCancel }) => {
const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActions, onCancel }) => {
const handleClick = () => {
onCancel!();
};
@ -33,17 +35,18 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ status, hideActions, onCanc
}
return (
<Stack space={2} className='p-4 rounded-lg bg-gray-100 dark:bg-gray-800'>
<Stack space={2} className={clsx('rounded-lg bg-gray-100 p-4 dark:bg-gray-800', className)}>
<AccountContainer
{...actions}
id={status.getIn(['account', 'id']) as string}
timestamp={status.created_at}
showProfileHoverCard={false}
withLinkToProfile={false}
hideActions={hideActions}
/>
<Text
className='break-words status__content'
className='status__content break-words'
size='sm'
dangerouslySetInnerHTML={{ __html: status.contentHtml }}
direction={isRtl(status.search_index) ? 'rtl' : 'ltr'}

View File

@ -10,7 +10,7 @@ import { makeGetStatus } from 'soapbox/selectors';
import type { Status as StatusEntity } from 'soapbox/types/entities';
interface IReplyMentions {
composeId: string,
composeId: string
}
const ReplyMentions: React.FC<IReplyMentions> = ({ composeId }) => {

View File

@ -12,8 +12,8 @@ const messages = defineMessages({
});
interface IScheduleButton {
composeId: string,
disabled?: boolean,
composeId: string
disabled?: boolean
}
const ScheduleButton: React.FC<IScheduleButton> = ({ composeId, disabled }) => {

View File

@ -1,6 +1,6 @@
'use strict';
import classNames from 'clsx';
import clsx from 'clsx';
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@ -28,7 +28,7 @@ const messages = defineMessages({
});
export interface IScheduleForm {
composeId: string,
composeId: string
}
const ScheduleForm: React.FC<IScheduleForm> = ({ composeId }) => {
@ -68,13 +68,13 @@ const ScheduleForm: React.FC<IScheduleForm> = ({ composeId }) => {
placeholderText={intl.formatMessage(messages.schedule)}
filterDate={isCurrentOrFutureDate}
filterTime={isFiveMinutesFromNow}
className={classNames({
className={clsx({
'has-error': !isFiveMinutesFromNow(scheduledAt),
})}
/>)}
</BundleContainer>
<IconButton
iconClassName='w-4 h-4'
iconClassName='h-4 w-4'
className='bg-transparent text-gray-400 hover:text-gray-600'
src={require('@tabler/icons/x.svg')}
onClick={handleRemove}

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
@ -9,11 +9,13 @@ import IconButton from 'soapbox/components/icon-button';
import ScrollableList from 'soapbox/components/scrollable-list';
import { HStack, Tabs, Text } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account-container';
import GroupContainer from 'soapbox/containers/group-container';
import StatusContainer from 'soapbox/containers/status-container';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder-account';
import PlaceholderGroupCard from 'soapbox/features/placeholder/components/placeholder-group-card';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder-hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder-status';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import type { OrderedSet as ImmutableOrderedSet } from 'immutable';
import type { VirtuosoHandle } from 'react-virtuoso';
@ -22,6 +24,7 @@ import type { SearchFilter } from 'soapbox/reducers/search';
const messages = defineMessages({
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
groups: { id: 'search_results.groups', defaultMessage: 'Groups' },
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
});
@ -30,6 +33,7 @@ const SearchResults = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const value = useAppSelector((state) => state.search.submittedValue);
const results = useAppSelector((state) => state.search.results);
@ -48,7 +52,8 @@ const SearchResults = () => {
const selectFilter = (newActiveFilter: SearchFilter) => dispatch(setFilter(newActiveFilter));
const renderFilterBar = () => {
const items = [
const items = [];
items.push(
{
text: intl.formatMessage(messages.accounts),
action: () => selectFilter('accounts'),
@ -59,12 +64,23 @@ const SearchResults = () => {
action: () => selectFilter('statuses'),
name: 'statuses',
},
);
if (features.groups) items.push(
{
text: intl.formatMessage(messages.groups),
action: () => selectFilter('groups'),
name: 'groups',
},
);
items.push(
{
text: intl.formatMessage(messages.hashtags),
action: () => selectFilter('hashtags'),
name: 'hashtags',
},
];
);
return <Tabs items={items} activeItem={selectedFilter} />;
};
@ -170,6 +186,31 @@ const SearchResults = () => {
}
}
if (selectedFilter === 'groups') {
hasMore = results.groupsHasMore;
loaded = results.groupsLoaded;
placeholderComponent = PlaceholderGroupCard;
if (results.groups && results.groups.size > 0) {
searchResults = results.groups.map((groupId: string) => (
<GroupContainer id={groupId} />
));
resultsIds = results.groups;
} else if (!submitted && trendingStatuses && !trendingStatuses.isEmpty()) {
searchResults = null;
} else if (loaded) {
noResultsMessage = (
<div className='empty-column-indicator'>
<FormattedMessage
id='empty_column.search.groups'
defaultMessage='There are no groups results for "{term}"'
values={{ term: value }}
/>
</div>
);
}
}
if (selectedFilter === 'hashtags') {
hasMore = results.hashtagsHasMore;
loaded = results.hashtagsLoaded;
@ -195,7 +236,7 @@ const SearchResults = () => {
return (
<>
{filterByAccount ? (
<HStack className='mb-4 pb-4 px-2 border-solid border-b border-gray-200 dark:border-gray-800' space={2}>
<HStack className='mb-4 border-b border-solid border-gray-200 px-2 pb-4 dark:border-gray-800' space={2}>
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleUnsetAccount} />
<Text>
<FormattedMessage
@ -219,10 +260,10 @@ const SearchResults = () => {
onLoadMore={handleLoadMore}
placeholderComponent={placeholderComponent}
placeholderCount={20}
className={classNames({
className={clsx({
'divide-gray-200 dark:divide-gray-800 divide-solid divide-y': selectedFilter === 'statuses',
})}
itemClassName={classNames({
itemClassName={clsx({
'pb-4': selectedFilter === 'accounts',
'pb-3': selectedFilter === 'hashtags',
})}

View File

@ -1,9 +1,7 @@
import classNames from 'clsx';
import { Map as ImmutableMap } from 'immutable';
import clsx from 'clsx';
import debounce from 'lodash/debounce';
import React, { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { useDispatch } from 'react-redux';
import { useHistory } from 'react-router-dom';
import {
@ -17,7 +15,8 @@ import {
import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input';
import { Input } from 'soapbox/components/ui';
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import { useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { AppDispatch, RootState } from 'soapbox/store';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
@ -25,7 +24,7 @@ const messages = defineMessages({
});
function redirectToAccount(accountId: string, routerHistory: any) {
return (_dispatch: any, getState: () => ImmutableMap<string, any>) => {
return (_dispatch: AppDispatch, getState: () => RootState) => {
const acct = getState().getIn(['accounts', accountId, 'acct']);
if (acct && routerHistory) {
@ -35,9 +34,9 @@ function redirectToAccount(accountId: string, routerHistory: any) {
}
interface ISearch {
autoFocus?: boolean,
autoSubmit?: boolean,
autosuggest?: boolean,
autoFocus?: boolean
autoSubmit?: boolean
autosuggest?: boolean
openInRoute?: boolean
}
@ -49,7 +48,7 @@ const Search = (props: ISearch) => {
openInRoute = false,
} = props;
const dispatch = useDispatch();
const dispatch = useAppDispatch();
const history = useHistory();
const intl = useIntl();
@ -150,17 +149,17 @@ const Search = (props: ISearch) => {
<div
role='button'
tabIndex={0}
className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto px-3 flex items-center cursor-pointer'
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto'
onClick={handleClear}
>
<SvgIcon
src={require('@tabler/icons/search.svg')}
className={classNames('h-4 w-4 text-gray-600', { hidden: hasValue })}
className={clsx('h-4 w-4 text-gray-600', { hidden: hasValue })}
/>
<SvgIcon
src={require('@tabler/icons/x.svg')}
className={classNames('h-4 w-4 text-gray-600', { hidden: !hasValue })}
className={clsx('h-4 w-4 text-gray-600', { hidden: !hasValue })}
aria-label={intl.formatMessage(messages.placeholder)}
/>
</div>

View File

@ -12,7 +12,7 @@ const messages = defineMessages({
});
interface ISpoilerButton {
composeId: string,
composeId: string
}
const SpoilerButton: React.FC<ISpoilerButton> = ({ composeId }) => {

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
@ -14,7 +14,7 @@ const messages = defineMessages({
});
interface ISpoilerInput extends Pick<IAutosuggestInput, 'onSuggestionsFetchRequested' | 'onSuggestionsClearRequested' | 'onSuggestionSelected'> {
composeId: string extends 'default' ? never : string,
composeId: string extends 'default' ? never : string
}
/** Text input for content warning in composer. */
@ -39,7 +39,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
return (
<Stack
space={4}
className={classNames({
className={clsx({
'relative transition-height': true,
'hidden': !compose.spoiler,
})}
@ -62,7 +62,7 @@ const SpoilerInput = React.forwardRef<AutosuggestInput, ISpoilerInput>(({
onSuggestionSelected={onSuggestionSelected}
searchTokens={[':']}
id='cw-spoiler-input'
className='rounded-md dark:!bg-transparent !bg-transparent'
className='rounded-md !bg-transparent dark:!bg-transparent'
ref={ref}
autoFocus
/>

View File

@ -1,17 +1,17 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React from 'react';
import { length } from 'stringz';
interface ITextCharacterCounter {
max: number,
text: string,
max: number
text: string
}
const TextCharacterCounter: React.FC<ITextCharacterCounter> = ({ text, max }) => {
const checkRemainingText = (diff: number) => {
return (
<span
className={classNames('text-sm font-medium', {
className={clsx('text-sm font-medium', {
'text-gray-700': diff >= 0,
'text-secondary-600': diff < 0,
})}

View File

@ -1,36 +0,0 @@
import React from 'react';
interface ITextIconButton {
label: string,
title: string,
active: boolean,
onClick: () => void,
ariaControls: string,
unavailable: boolean,
}
const TextIconButton: React.FC<ITextIconButton> = ({
label,
title,
active,
ariaControls,
unavailable,
onClick,
}) => {
const handleClick: React.MouseEventHandler = (e) => {
e.preventDefault();
onClick();
};
if (unavailable) {
return null;
}
return (
<button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={handleClick} aria-controls={ariaControls}>
{label}
</button>
);
};
export default TextIconButton;

View File

@ -15,13 +15,14 @@ const onlyImages = (types: ImmutableList<string>) => {
};
export interface IUploadButton {
disabled?: boolean,
unavailable?: boolean,
onSelectFile: (files: FileList, intl: IntlShape) => void,
style?: React.CSSProperties,
resetFileKey: number | null,
className?: string,
iconClassName?: string,
disabled?: boolean
unavailable?: boolean
onSelectFile: (files: FileList, intl: IntlShape) => void
style?: React.CSSProperties
resetFileKey: number | null
className?: string
iconClassName?: string
icon?: string
}
const UploadButton: React.FC<IUploadButton> = ({
@ -31,6 +32,7 @@ const UploadButton: React.FC<IUploadButton> = ({
resetFileKey,
className = 'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500',
iconClassName,
icon,
}) => {
const intl = useIntl();
const { configuration } = useInstance();
@ -52,9 +54,11 @@ const UploadButton: React.FC<IUploadButton> = ({
return null;
}
const src = onlyImages(attachmentTypes)
? require('@tabler/icons/photo.svg')
: require('@tabler/icons/paperclip.svg');
const src = icon || (
onlyImages(attachmentTypes)
? require('@tabler/icons/photo.svg')
: require('@tabler/icons/paperclip.svg')
);
return (
<div>

View File

@ -1,4 +1,4 @@
import classNames from 'clsx';
import clsx from 'clsx';
import React from 'react';
import { HStack } from 'soapbox/components/ui';
@ -10,7 +10,7 @@ import UploadProgress from './upload-progress';
import type { Attachment as AttachmentEntity } from 'soapbox/types/entities';
interface IUploadForm {
composeId: string,
composeId: string
}
const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
@ -20,7 +20,7 @@ const UploadForm: React.FC<IUploadForm> = ({ composeId }) => {
<div className='overflow-hidden'>
<UploadProgress composeId={composeId} />
<HStack wrap className={classNames(mediaIds.size !== 0 && 'p-1')}>
<HStack wrap className={clsx('overflow-hidden', mediaIds.size !== 0 && 'p-1')}>
{mediaIds.map((id: string) => (
<Upload id={id} key={id} composeId={composeId} />
))}

View File

@ -4,7 +4,7 @@ import UploadProgress from 'soapbox/components/upload-progress';
import { useCompose } from 'soapbox/hooks';
interface IComposeUploadProgress {
composeId: string,
composeId: string
}
/** File upload progress bar for post composer. */

View File

@ -6,8 +6,8 @@ import Upload from 'soapbox/components/upload';
import { useAppDispatch, useCompose, useInstance } from 'soapbox/hooks';
interface IUploadCompose {
id: string,
composeId: string,
id: string
composeId: string
}
const UploadCompose: React.FC<IUploadCompose> = ({ composeId, id }) => {

View File

@ -5,14 +5,14 @@ import { length } from 'stringz';
import ProgressCircle from 'soapbox/components/progress-circle';
const messages = defineMessages({
title: { id: 'compose.character_counter.title', defaultMessage: 'Used {chars} out of {maxChars} characters' },
title: { id: 'compose.character_counter.title', defaultMessage: 'Used {chars} out of {maxChars} {maxChars, plural, one {character} other {characters}}' },
});
interface IVisualCharacterCounter {
/** max text allowed */
max: number,
max: number
/** text to use to measure */
text: string,
text: string
}
/** Renders a character counter */

View File

@ -4,7 +4,7 @@ import { spring } from 'react-motion';
import Motion from '../../ui/util/optional-motion';
interface IWarning {
message: React.ReactNode,
message: React.ReactNode
}
/** Warning message displayed in ComposeForm. */