Migrate to external library for interacting with API

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak
2024-08-04 16:09:52 +02:00
parent 963ffb3d45
commit 4dfdfcccd3
137 changed files with 1136 additions and 2094 deletions

View File

@@ -91,7 +91,6 @@ const Announcements: React.FC = () => {
const { data: announcements, isLoading } = useAnnouncements();
const handleCreateAnnouncement = () => {
dispatch(openModal('EDIT_ANNOUNCEMENT'));
};

View File

@@ -9,7 +9,6 @@ import { useFeatures } from 'soapbox/hooks';
import NewFolderForm from './components/new-folder-form';
const messages = defineMessages({
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
});

View File

@@ -7,7 +7,7 @@ import { Button, Combobox, ComboboxInput, ComboboxList, ComboboxOption, Combobox
import { useChatContext } from 'soapbox/contexts/chat-context';
import UploadButton from 'soapbox/features/compose/components/upload-button';
import emojiSearch from 'soapbox/features/emoji/search';
import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { Attachment } from 'soapbox/types/entities';
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
@@ -45,9 +45,9 @@ interface IChatComposer extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaEl
onSelectFile: (files: FileList, intl: IntlShape) => void;
resetFileKey: number | null;
resetContentKey: number | null;
attachments?: Attachment[];
onDeleteAttachment?: (i: number) => void;
uploadCount?: number;
attachment?: Attachment | null;
onDeleteAttachment?: () => void;
uploading?: boolean;
uploadProgress?: number;
}
@@ -63,29 +63,25 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
resetFileKey,
resetContentKey,
onPaste,
attachments = [],
attachment,
onDeleteAttachment,
uploadCount = 0,
uploading,
uploadProgress,
}, ref) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const features = useFeatures();
const { chat } = useChatContext();
const isBlocked = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocked_by']));
const isBlocking = useAppSelector((state) => state.getIn(['relationships', chat?.account?.id, 'blocking']));
const maxCharacterCount = useAppSelector((state) => state.instance.configuration.chats.max_characters);
const attachmentLimit = useAppSelector(state => state.instance.configuration.chats.max_media_attachments);
const maxCharacterCount = useInstance().configuration.chats.max_characters;
const [suggestions, setSuggestions] = useState<Suggestion>(initialSuggestionState);
const isSuggestionsAvailable = suggestions.list.length > 0;
const isUploading = uploadCount > 0;
const hasAttachment = attachments.length > 0;
const isOverCharacterLimit = maxCharacterCount && value?.length > maxCharacterCount;
const isSubmitDisabled = disabled || isUploading || isOverCharacterLimit || (value.length === 0 && !hasAttachment);
const isSubmitDisabled = disabled || uploading || isOverCharacterLimit || (value.length === 0 && !attachment);
const overLimitText = maxCharacterCount ? maxCharacterCount - value?.length : '';
@@ -170,17 +166,15 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
<div className='h-5' />
<HStack alignItems='stretch' justifyContent='between' space={4}>
{features.chatsMedia && (
<Stack justifyContent='end' alignItems='center' className='mb-1.5 w-10'>
<UploadButton
onSelectFile={onSelectFile}
resetFileKey={resetFileKey}
iconClassName='h-5 w-5'
className='text-primary-500'
disabled={isUploading || (attachments.length >= attachmentLimit)}
/>
</Stack>
)}
<Stack justifyContent='end' alignItems='center' className='mb-1.5 w-10'>
<UploadButton
onSelectFile={onSelectFile}
resetFileKey={resetFileKey}
iconClassName='h-5 w-5'
className='text-primary-500'
disabled={uploading || !!attachment}
/>
</Stack>
<Stack grow>
<Combobox onSelect={onSelectComboboxOption}>
@@ -198,9 +192,9 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
autoGrow
maxRows={5}
disabled={disabled}
attachments={attachments}
attachment={attachment}
onDeleteAttachment={onDeleteAttachment}
uploadCount={uploadCount}
uploading={uploading}
uploadProgress={uploadProgress}
/>
{isSuggestionsAvailable ? (

View File

@@ -7,30 +7,21 @@ import ChatPendingUpload from './chat-pending-upload';
import ChatUpload from './chat-upload';
interface IChatTextarea extends React.ComponentProps<typeof Textarea> {
attachments?: Attachment[];
onDeleteAttachment?: (i: number) => void;
uploadCount?: number;
attachment?: Attachment | null;
onDeleteAttachment?: () => void;
uploading?: boolean;
uploadProgress?: number;
}
/** Custom textarea for chats. */
const ChatTextarea: React.FC<IChatTextarea> = React.forwardRef(({
attachments,
attachment,
onDeleteAttachment,
uploadCount = 0,
uploading,
uploadProgress = 0,
...rest
}, ref) => {
const isUploading = uploadCount > 0;
const handleDeleteAttachment = (i: number) => () => {
if (onDeleteAttachment) {
onDeleteAttachment(i);
}
};
return (
<div className={`
}, ref) => (
<div className={`
block
w-full
rounded-md border border-gray-400
@@ -41,30 +32,29 @@ const ChatTextarea: React.FC<IChatTextarea> = React.forwardRef(({
dark:bg-gray-800 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600
dark:focus-within:border-primary-500 dark:focus-within:ring-primary-500
`}
>
{(!!attachments?.length || isUploading) && (
<HStack className='-ml-2 -mt-2 p-3 pb-0' wrap>
{attachments?.map((attachment, i) => (
<div className='ml-2 mt-2 flex'>
<ChatUpload
key={attachment.id}
attachment={attachment}
onDelete={handleDeleteAttachment(i)}
/>
</div>
))}
>
{(attachment || uploading) && (
<HStack className='-ml-2 -mt-2 p-3 pb-0' wrap>
{attachment && (
<div className='ml-2 mt-2 flex'>
<ChatUpload
key={attachment.id}
attachment={attachment}
onDelete={onDeleteAttachment}
/>
</div>
)}
{Array.from(Array(uploadCount)).map(() => (
<div className='ml-2 mt-2 flex'>
<ChatPendingUpload progress={uploadProgress} />
</div>
))}
</HStack>
)}
{uploading && (
<div className='ml-2 mt-2 flex'>
<ChatPendingUpload progress={uploadProgress} />
</div>
)}
</HStack>
)}
<Textarea ref={ref} theme='transparent' {...rest} />
</div>
);
});
<Textarea ref={ref} theme='transparent' {...rest} />
</div>
));
export { ChatTextarea as default };

View File

@@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { uploadMedia } from 'soapbox/actions/media';
import { Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers';
import { IChat, useChatActions } from 'soapbox/queries/chats';
import toast from 'soapbox/toast';
@@ -53,20 +53,19 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const dispatch = useAppDispatch();
const { createChatMessage } = useChatActions(chat.id);
const attachmentLimit = useAppSelector(state => state.instance.configuration.chats.max_media_attachments);
const [content, setContent] = useState<string>('');
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [uploadCount, setUploadCount] = useState(0);
const [attachment, setAttachment] = useState<Attachment | null>(null);
const [uploading, setUploading] = useState(false);
const [uploadProgress, setUploadProgress] = useState(0);
const [resetContentKey, setResetContentKey] = useState<number>(fileKeyGen());
const [resetFileKey, setResetFileKey] = useState<number>(fileKeyGen());
const [errorMessage, setErrorMessage] = useState<string>();
const isSubmitDisabled = content.length === 0 && attachments.length === 0;
const isSubmitDisabled = content.length === 0 && !attachment;
const submitMessage = () => {
createChatMessage.mutate({ chatId: chat.id, content, mediaIds: attachments.map(a => a.id) }, {
createChatMessage.mutate({ chatId: chat.id, content, mediaId: attachment?.id }, {
onSuccess: () => {
setErrorMessage(undefined);
},
@@ -85,8 +84,8 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
clearNativeInputValue(inputRef.current);
}
setContent('');
setAttachments([]);
setUploadCount(0);
setAttachment(null);
setUploading(false);
setUploadProgress(0);
setResetFileKey(fileKeyGen());
setResetContentKey(fileKeyGen());
@@ -129,10 +128,8 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
const handleMouseOver = () => markRead();
const handleRemoveFile = (i: number) => {
const newAttachments = [...attachments];
newAttachments.splice(i, 1);
setAttachments(newAttachments);
const handleRemoveFile = () => {
setAttachment(null);
setResetFileKey(fileKeyGen());
};
@@ -142,26 +139,20 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
};
const handleFiles = (files: FileList) => {
if (files.length + attachments.length > attachmentLimit) {
if (attachment) {
toast.error(messages.uploadErrorLimit);
return;
}
setUploadCount(files.length);
setUploading(true);
const promises = Array.from(files).map(async(file) => {
const data = new FormData();
data.append('file', file);
const response = await dispatch(uploadMedia(data, onUploadProgress));
return normalizeAttachment(response.json);
});
dispatch(uploadMedia({ file: files[0] }, onUploadProgress)).then(response => {
const newAttachment = normalizeAttachment(response);
return Promise.all(promises)
.then((newAttachments) => {
setAttachments([...attachments, ...newAttachments]);
setUploadCount(0);
})
.catch(() => setUploadCount(0));
setAttachment(newAttachment);
setUploading(false);
})
.catch(() => setUploading(false));
};
useEffect(() => {
@@ -187,9 +178,9 @@ const Chat: React.FC<ChatInterface> = ({ chat, inputRef, className }) => {
resetFileKey={resetFileKey}
resetContentKey={resetContentKey}
onPaste={handlePaste}
attachments={attachments}
attachment={attachment}
onDeleteAttachment={handleRemoveFile}
uploadCount={uploadCount}
uploading={uploading}
uploadProgress={uploadProgress}
/>
</Stack>

View File

@@ -51,7 +51,6 @@ const LanguageDropdown: React.FC<ILanguageDropdown> = ({ composeId }) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [searchValue, setSearchValue] = useState('');
const { x, y, strategy, refs, middlewareData, placement } = useFloating<HTMLButtonElement>({
placement: 'top',
middleware: [

View File

@@ -135,7 +135,6 @@ const PrivacyDropdownMenu: React.FC<IPrivacyDropdownMenu> = ({
};
}, []);
return (
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
{({ opacity, scaleX, scaleY }) => (

View File

@@ -1,55 +0,0 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { directComposeById } from 'soapbox/actions/compose';
import { expandDirectTimeline } from 'soapbox/actions/timelines';
import { useDirectStream } from 'soapbox/api/hooks';
import AccountSearch from 'soapbox/components/account-search';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
const messages = defineMessages({
heading: { id: 'column.direct', defaultMessage: 'Direct messages' },
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
});
const DirectTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const next = useAppSelector(state => state.timelines.get('direct')?.next);
useDirectStream();
useEffect(() => {
dispatch(expandDirectTimeline({}, intl));
}, []);
const handleSuggestion = (accountId: string) => {
dispatch(directComposeById(accountId));
};
const handleLoadMore = (maxId: string) => {
dispatch(expandDirectTimeline({ url: next, maxId }, intl));
};
return (
<Column label={intl.formatMessage(messages.heading)}>
<AccountSearch
placeholder={intl.formatMessage(messages.searchPlaceholder)}
onSelected={handleSuggestion}
/>
<Timeline
scrollKey='direct_timeline'
timelineId='direct'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
divideType='border'
/>
</Column>
);
};
export { DirectTimeline as default };

View File

@@ -11,7 +11,7 @@ interface ICSVExporter {
input_hint: MessageDescriptor;
submit: MessageDescriptor;
};
action: () => (dispatch: AppDispatch, getState: () => RootState) => Promise<void>;
action: () => (dispatch: AppDispatch, getState: () => RootState) => Promise<any>;
}
const CSVExporter: React.FC<ICSVExporter> = ({ messages, action }) => {

View File

@@ -91,7 +91,7 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
const [notFound, setNotFound] = useState(false);
const [title, setTitle] = useState('');
const [expiresIn, setExpiresIn] = useState<string | null>(null);
const [expiresIn, setExpiresIn] = useState<number | undefined>();
const [homeTimeline, setHomeTimeline] = useState(true);
const [publicTimeline, setPublicTimeline] = useState(false);
const [notifications, setNotifications] = useState(false);
@@ -111,7 +111,7 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
}), []);
const handleSelectChange: React.ChangeEventHandler<HTMLSelectElement> = e => {
setExpiresIn(e.target.value);
setExpiresIn(+e.target.value || undefined);
};
const handleAddNew: React.FormEventHandler = e => {
@@ -189,15 +189,13 @@ const EditFilter: React.FC<IEditFilter> = ({ params }) => {
/>
</FormGroup>
{features.filtersExpiration && (
<FormGroup labelText={intl.formatMessage(messages.expires)}>
<SelectDropdown
items={expirations}
defaultValue=''
onChange={handleSelectChange}
/>
</FormGroup>
)}
<FormGroup labelText={intl.formatMessage(messages.expires)}>
<SelectDropdown
items={expirations}
defaultValue=''
onChange={handleSelectChange}
/>
</FormGroup>
<Stack>
<Text size='sm' weight='medium'>

View File

@@ -1,4 +1,3 @@
import debounce from 'lodash/debounce';
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
@@ -17,19 +16,10 @@ const FollowRecommendations: React.FC = () => {
const intl = useIntl();
const suggestions = useAppSelector((state) => state.suggestions.items);
const hasMore = useAppSelector((state) => !!state.suggestions.next);
const isLoading = useAppSelector((state) => state.suggestions.isLoading);
const handleLoadMore = debounce(() => {
if (isLoading) {
return null;
}
return dispatch(fetchSuggestions({ limit: 20 }));
}, 300);
useEffect(() => {
dispatch(fetchSuggestions({ limit: 20 }));
dispatch(fetchSuggestions(20));
}, []);
if (suggestions.size === 0 && !isLoading) {
@@ -48,8 +38,6 @@ const FollowRecommendations: React.FC = () => {
<ScrollableList
isLoading={isLoading}
scrollKey='suggestions'
onLoadMore={handleLoadMore}
hasMore={hasMore}
itemClassName='pb-4'
>
{suggestions.map((suggestion) => (

View File

@@ -10,7 +10,6 @@ const messages = defineMessages({
heading: { id: 'column.mutes', defaultMessage: 'Mutes' },
});
const Mutes: React.FC = () => {
const intl = useIntl();

View File

@@ -56,7 +56,7 @@ const NotificationFilterBar = () => {
action: onClick('mention'),
name: 'mention',
});
if (features.accountNotifies || features.accountSubscriptions) items.push({
if (features.accountNotifies) items.push({
text: <Icon className='h-4 w-4' src={require('@tabler/icons/outline/bell-ringing.svg')} />,
title: intl.formatMessage(messages.statuses),
action: onClick('status'),

View File

@@ -1,4 +1,3 @@
import debounce from 'lodash/debounce';
import React from 'react';
import { FormattedMessage } from 'react-intl';
@@ -9,15 +8,7 @@ import AccountContainer from 'soapbox/containers/account-container';
import { useOnboardingSuggestions } from 'soapbox/queries/suggestions';
const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions();
const handleLoadMore = debounce(() => {
if (isFetching) {
return null;
}
return fetchNextPage();
}, 300);
const { data, isFetching } = useOnboardingSuggestions();
const renderSuggestions = () => {
if (!data) {
@@ -29,8 +20,6 @@ const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => {
<ScrollableList
isLoading={isFetching}
scrollKey='suggestions'
onLoadMore={handleLoadMore}
hasMore={hasNextPage}
useWindowScroll={false}
style={{ height: 320 }}
>

View File

@@ -119,9 +119,7 @@ const SoapboxConfig: React.FC = () => {
const file = e.target.files?.item(0);
if (file) {
data.append('file', file);
dispatch(uploadMedia(data)).then(({ data }: any) => {
dispatch(uploadMedia({ file})).then((data: any) => {
handleChange(path, () => data.url)(e);
}).catch(console.error);
}

View File

@@ -54,7 +54,6 @@ const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
const handleImageClear = (property: keyof CreateGroupParams) => () => onChange({ [property]: undefined });
return (
<Form>
<div className='relative mb-12 flex'>

View File

@@ -14,7 +14,6 @@ import { isStandalone } from 'soapbox/utils/state';
import type { PlfeResponse } from 'soapbox/api';
const SignUpPanel = () => {
const dispatch = useAppDispatch();
const instance = useInstance();

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import {
subscribeAccount,
unsubscribeAccount,
} from 'soapbox/actions/accounts';
import { useFollow } from 'soapbox/api/hooks';
import { IconButton } from 'soapbox/components/ui';
import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import { useFeatures } from 'soapbox/hooks';
import toast from 'soapbox/toast';
import type { Account as AccountEntity } from 'soapbox/types/entities';
@@ -26,7 +22,6 @@ interface ISubscriptionButton {
}
const SubscriptionButton = ({ account }: ISubscriptionButton) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const { follow } = useFollow();
@@ -40,51 +35,23 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => {
? intl.formatMessage(messages.unsubscribe, { name: account.username })
: intl.formatMessage(messages.subscribe, { name: account.username });
const onSubscribeSuccess = () =>
toast.success(intl.formatMessage(messages.subscribeSuccess));
const onSubscribeFailure = () =>
toast.error(intl.formatMessage(messages.subscribeFailure));
const onUnsubscribeSuccess = () =>
toast.success(intl.formatMessage(messages.unsubscribeSuccess));
const onUnsubscribeFailure = () =>
toast.error(intl.formatMessage(messages.unsubscribeFailure));
const onNotifyToggle = () => {
if (account.relationship?.notifying) {
follow(account.id, { notify: false })
?.then(() => onUnsubscribeSuccess())
.catch(() => onUnsubscribeFailure());
?.then(() => toast.success(intl.formatMessage(messages.unsubscribeSuccess)))
.catch(() => toast.error(intl.formatMessage(messages.unsubscribeFailure)));
} else {
follow(account.id, { notify: true })
?.then(() => onSubscribeSuccess())
.catch(() => onSubscribeFailure());
}
};
const onSubscriptionToggle = () => {
if (account.relationship?.subscribing) {
dispatch(unsubscribeAccount(account.id))
?.then(() => onUnsubscribeSuccess())
.catch(() => onUnsubscribeFailure());
} else {
dispatch(subscribeAccount(account.id))
?.then(() => onSubscribeSuccess())
.catch(() => onSubscribeFailure());
?.then(() => toast.success(intl.formatMessage(messages.subscribeSuccess)))
.catch(() => toast.error(intl.formatMessage(messages.subscribeFailure)));
}
};
const handleToggle = () => {
if (features.accountNotifies) {
onNotifyToggle();
} else {
onSubscriptionToggle();
}
onNotifyToggle();
};
if (!features.accountSubscriptions && !features.accountNotifies) {
if (!features.accountNotifies) {
return null;
}

View File

@@ -50,7 +50,6 @@ import {
HomeTimeline,
Followers,
Following,
DirectTimeline,
Conversations,
HashtagTimeline,
Notifications,
@@ -181,10 +180,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
{features.federating && <WrappedRoute path='/timeline/:instance' exact page={RemoteInstancePage} component={RemoteTimeline} content={children} />}
{features.conversations && <WrappedRoute path='/conversations' page={DefaultPage} component={Conversations} content={children} />}
{features.directTimeline && <WrappedRoute path='/messages' page={DefaultPage} component={DirectTimeline} content={children} />}
{(features.conversations && !features.directTimeline) && (
<WrappedRoute path='/messages' page={DefaultPage} component={Conversations} content={children} />
)}
{features.conversations && <Redirect from='/messages' to='/conversations' />}
{/* Mastodon web routes */}
<Redirect from='/web/:path1/:path2/:path3' to='/:path1/:path2/:path3' />

View File

@@ -9,7 +9,6 @@ export const PublicTimeline = lazy(() => import('soapbox/features/public-timelin
export const RemoteTimeline = lazy(() => import('soapbox/features/remote-timeline'));
export const CommunityTimeline = lazy(() => import('soapbox/features/community-timeline'));
export const HashtagTimeline = lazy(() => import('soapbox/features/hashtag-timeline'));
export const DirectTimeline = lazy(() => import('soapbox/features/direct-timeline'));
export const Conversations = lazy(() => import('soapbox/features/conversations'));
export const ListTimeline = lazy(() => import('soapbox/features/list-timeline'));
export const Lists = lazy(() => import('soapbox/features/lists'));