@ -14,6 +14,7 @@ import {
|
||||
CryptoDonateModal,
|
||||
DislikesModal,
|
||||
EditAnnouncementModal,
|
||||
EditBookmarkFolderModal,
|
||||
EditFederationModal,
|
||||
EmbedModal,
|
||||
EventMapModal,
|
||||
@ -36,6 +37,7 @@ import {
|
||||
ReblogsModal,
|
||||
ReplyMentionsModal,
|
||||
ReportModal,
|
||||
SelectBookmarkFolderModal,
|
||||
UnauthorizedModal,
|
||||
VideoModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
@ -57,6 +59,7 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
|
||||
'CRYPTO_DONATE': CryptoDonateModal,
|
||||
'DISLIKES': DislikesModal,
|
||||
'EDIT_ANNOUNCEMENT': EditAnnouncementModal,
|
||||
'EDIT_BOOKMARK_FOLDER': EditBookmarkFolderModal,
|
||||
'EDIT_FEDERATION': EditFederationModal,
|
||||
'EMBED': EmbedModal,
|
||||
'EVENT_MAP': EventMapModal,
|
||||
@ -78,6 +81,7 @@ const MODAL_COMPONENTS: Record<string, React.LazyExoticComponent<any>> = {
|
||||
'REBLOGS': ReblogsModal,
|
||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||
'REPORT': ReportModal,
|
||||
'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal,
|
||||
'UNAUTHORIZED': UnauthorizedModal,
|
||||
'VIDEO': VideoModal,
|
||||
};
|
||||
|
||||
@ -12,7 +12,6 @@ import { useAppDispatch, useAppSelector, useCompose, useDraggedFiles } from 'soa
|
||||
import ComposeForm from '../../../compose/components/compose-form';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' },
|
||||
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
|
||||
});
|
||||
|
||||
160
src/features/ui/components/modals/edit-bookmark-folder-modal.tsx
Normal file
160
src/features/ui/components/modals/edit-bookmark-folder-modal.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { useFloating, shift } from '@floating-ui/react';
|
||||
import React, { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { useBookmarkFolder, useUpdateBookmarkFolder } from 'soapbox/api/hooks';
|
||||
import { Emoji, HStack, Icon, Input, Modal } from 'soapbox/components/ui';
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown';
|
||||
import { messages as emojiMessages } from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container';
|
||||
import { useAppDispatch, useClickOutside } from 'soapbox/hooks';
|
||||
import { useTextField } from 'soapbox/hooks/forms';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
import type { Emoji as EmojiType } from 'soapbox/features/emoji';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'bookmark_folders.new.title_placeholder', defaultMessage: 'New folder title' },
|
||||
editSuccess: { id: 'bookmark_folders.edit.success', defaultMessage: 'Bookmark folder edited successfully' },
|
||||
editFail: { id: 'bookmark_folders.edit.fail', defaultMessage: 'Failed to edit bookmark folder' },
|
||||
});
|
||||
|
||||
interface IEmojiPicker {
|
||||
emoji?: string;
|
||||
emojiUrl?: string;
|
||||
onPickEmoji?: (emoji: EmojiType) => void;
|
||||
}
|
||||
|
||||
const EmojiPicker: React.FC<IEmojiPicker> = ({ emoji, emojiUrl, ...props }) => {
|
||||
const intl = useIntl();
|
||||
const title = intl.formatMessage(emojiMessages.emoji);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
const { x, y, strategy, refs, update } = useFloating<HTMLButtonElement>({
|
||||
middleware: [shift()],
|
||||
});
|
||||
|
||||
useClickOutside(refs, () => {
|
||||
setVisible(false);
|
||||
});
|
||||
|
||||
const handleToggle: React.KeyboardEventHandler<HTMLButtonElement> & React.MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
setVisible(!visible);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
<button
|
||||
className='mt-1 flex h-[38px] w-[38px] items-center justify-center rounded-md border border-solid border-gray-400 bg-white text-gray-900 ring-1 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500'
|
||||
ref={refs.setReference}
|
||||
title={title}
|
||||
aria-label={title}
|
||||
aria-expanded={visible}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleToggle}
|
||||
tabIndex={0}
|
||||
>
|
||||
{emoji
|
||||
? <Emoji height={20} width={20} emoji={emoji} />
|
||||
: <Icon className='h-5 w-5 text-gray-600 hover:text-gray-700 dark:hover:text-gray-500' src={require('@tabler/icons/mood-happy.svg')} />}
|
||||
</button>
|
||||
|
||||
{createPortal(
|
||||
<div
|
||||
className='z-[101]'
|
||||
ref={refs.setFloating}
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
left: x ?? 0,
|
||||
width: 'max-content',
|
||||
}}
|
||||
>
|
||||
<EmojiPickerDropdown
|
||||
visible={visible}
|
||||
setVisible={setVisible}
|
||||
update={update}
|
||||
{...props}
|
||||
/>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IEditBookmarkFolderModal {
|
||||
folderId: string;
|
||||
onClose: (type: string) => void;
|
||||
}
|
||||
|
||||
const EditBookmarkFolderModal: React.FC<IEditBookmarkFolderModal> = ({ folderId, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { bookmarkFolder } = useBookmarkFolder(folderId);
|
||||
const { updateBookmarkFolder, isSubmitting } = useUpdateBookmarkFolder(folderId);
|
||||
|
||||
const [emoji, setEmoji] = useState(bookmarkFolder?.emoji);
|
||||
const [emojiUrl, setEmojiUrl] = useState(bookmarkFolder?.emoji_url);
|
||||
const name = useTextField(bookmarkFolder?.name);
|
||||
|
||||
const handleEmojiPick = (data: EmojiType) => {
|
||||
if (data.custom) {
|
||||
setEmojiUrl(data.imageUrl);
|
||||
setEmoji(data.colons);
|
||||
} else {
|
||||
setEmoji(data.native);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('EDIT_BOOKMARK_FOLDER');
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
updateBookmarkFolder({
|
||||
name: name.value,
|
||||
emoji,
|
||||
}).then(() => {
|
||||
toast.success(intl.formatMessage(messages.editSuccess));
|
||||
dispatch(closeModal('EDIT_BOOKMARK_FOLDER'));
|
||||
})
|
||||
.catch(() => {
|
||||
toast.success(intl.formatMessage(messages.editFail));
|
||||
});
|
||||
};
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='edit_bookmark_folder_modal.header_title' defaultMessage='Edit folder' />}
|
||||
onClose={onClickClose}
|
||||
confirmationAction={handleSubmit}
|
||||
confirmationText={<FormattedMessage id='edit_bookmark_folder_modal.confirm' defaultMessage='Save' />}
|
||||
>
|
||||
<HStack space={2}>
|
||||
<EmojiPicker
|
||||
emoji={emoji}
|
||||
emojiUrl={emojiUrl}
|
||||
onPickEmoji={handleEmojiPick}
|
||||
/>
|
||||
<label className='grow'>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={label}
|
||||
disabled={isSubmitting}
|
||||
{...name}
|
||||
/>
|
||||
</label>
|
||||
</HStack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditBookmarkFolderModal;
|
||||
@ -13,7 +13,6 @@ import { ReactionRecord } from 'soapbox/reducers/user-lists';
|
||||
import type { Item } from 'soapbox/components/ui/tabs/tabs';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
all: { id: 'reactions.all', defaultMessage: 'All' },
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { bookmark } from 'soapbox/actions/interactions';
|
||||
import { useBookmarkFolders } from 'soapbox/api/hooks';
|
||||
import { RadioGroup, RadioItem } from 'soapbox/components/radio';
|
||||
import { Emoji, HStack, Icon, Modal, Spinner, Stack } from 'soapbox/components/ui';
|
||||
import NewFolderForm from 'soapbox/features/bookmark-folders/components/new-folder-form';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetStatus } from 'soapbox/selectors';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
interface ISelectBookmarkFolderModal {
|
||||
statusId: string;
|
||||
onClose: (type: string) => void;
|
||||
}
|
||||
|
||||
const SelectBookmarkFolderModal: React.FC<ISelectBookmarkFolderModal> = ({ statusId, onClose }) => {
|
||||
const getStatus = useCallback(makeGetStatus(), []);
|
||||
const status = useAppSelector(state => getStatus(state, { id: statusId })) as StatusEntity;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [selectedFolder, setSelectedFolder] = useState(status.pleroma.get('bookmark_folder'));
|
||||
|
||||
const { isFetching, bookmarkFolders } = useBookmarkFolders();
|
||||
|
||||
const onChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const folderId = e.target.value;
|
||||
setSelectedFolder(folderId);
|
||||
|
||||
dispatch(bookmark(status, folderId)).then(() => {
|
||||
onClose('SELECT_BOOKMARK_FOLDER');
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('SELECT_BOOKMARK_FOLDER');
|
||||
};
|
||||
|
||||
const items = [
|
||||
<RadioItem
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/bookmarks.svg')} size={20} />
|
||||
<span><FormattedMessage id='bookmark_folders.all_bookmarks' defaultMessage='All bookmarks' /></span>
|
||||
</HStack>
|
||||
}
|
||||
checked={selectedFolder === null}
|
||||
value={''}
|
||||
/>,
|
||||
];
|
||||
|
||||
if (!isFetching) {
|
||||
items.push(...(bookmarkFolders.map((folder) => (
|
||||
<RadioItem
|
||||
key={folder.id}
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
{folder.emoji ? (
|
||||
<Emoji
|
||||
emoji={folder.emoji}
|
||||
src={folder.emoji_url || undefined}
|
||||
className='h-5 w-5 flex-none'
|
||||
/>
|
||||
) : <Icon src={require('@tabler/icons/folder.svg')} size={20} />}
|
||||
<span>{folder.name}</span>
|
||||
</HStack>
|
||||
}
|
||||
checked={selectedFolder === folder.id}
|
||||
value={folder.id}
|
||||
/>
|
||||
))));
|
||||
}
|
||||
|
||||
const body = isFetching ? <Spinner /> : (
|
||||
<Stack space={4}>
|
||||
<NewFolderForm />
|
||||
|
||||
<RadioGroup onChange={onChange}>
|
||||
{items}
|
||||
</RadioGroup>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='select_bookmark_folder_modal.header_title' defaultMessage='Select folder' />}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
{body}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SelectBookmarkFolderModal;
|
||||
@ -9,7 +9,6 @@ import { selectAccount } from 'soapbox/selectors';
|
||||
import toast from 'soapbox/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
accountPlaceholder: { id: 'remote_interaction.account_placeholder', defaultMessage: 'Enter your username@domain you want to act from' },
|
||||
userNotFoundError: { id: 'remote_interaction.user_not_found_error', defaultMessage: 'Couldn\'t find given user' },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user