Merge, store Lexical editorState
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
@ -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 }) => {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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,
|
||||
})}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}), []);
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -13,7 +13,7 @@ const messages = defineMessages({
|
||||
|
||||
interface IPollButton {
|
||||
composeId: string
|
||||
disabled?: boolean,
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const PollButton: React.FC<IPollButton> = ({ composeId, disabled }) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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'}
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -12,8 +12,8 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
interface IScheduleButton {
|
||||
composeId: string,
|
||||
disabled?: boolean,
|
||||
composeId: string
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const ScheduleButton: React.FC<IScheduleButton> = ({ composeId, disabled }) => {
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
})}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -12,7 +12,7 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
interface ISpoilerButton {
|
||||
composeId: string,
|
||||
composeId: string
|
||||
}
|
||||
|
||||
const SpoilerButton: React.FC<ISpoilerButton> = ({ composeId }) => {
|
||||
|
||||
@ -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
|
||||
/>
|
||||
|
||||
@ -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,
|
||||
})}
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
|
||||
@ -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} />
|
||||
))}
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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 }) => {
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user