kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'remove-bookmark-folder' into 'main'
Remove bookmark folders See merge request soapbox-pub/soapbox!3276merge-requests/3281/head
commit
edd4b692ef
|
@ -15,77 +15,71 @@ const BOOKMARKED_STATUSES_EXPAND_FAIL = 'BOOKMARKED_STATUSES_EXPAND_FAIL';
|
|||
|
||||
const noOp = () => new Promise(f => f(undefined));
|
||||
|
||||
const fetchBookmarkedStatuses = (folderId?: string) =>
|
||||
const fetchBookmarkedStatuses = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
if (getState().status_lists.get(folderId ? `bookmarks:${folderId}` : 'bookmarks')?.isLoading) {
|
||||
if (getState().status_lists.get('bookmarks')?.isLoading) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(fetchBookmarkedStatusesRequest(folderId));
|
||||
dispatch(fetchBookmarkedStatusesRequest());
|
||||
|
||||
return api(getState).get(`/api/v1/bookmarks${folderId ? `?folder_id=${folderId}` : ''}`).then(response => {
|
||||
return api(getState).get('/api/v1/bookmarks').then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null, folderId));
|
||||
return dispatch(fetchBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(fetchBookmarkedStatusesFail(error, folderId));
|
||||
dispatch(fetchBookmarkedStatusesFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const fetchBookmarkedStatusesRequest = (folderId?: string) => ({
|
||||
const fetchBookmarkedStatusesRequest = () => ({
|
||||
type: BOOKMARKED_STATUSES_FETCH_REQUEST,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const fetchBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null, folderId?: string) => ({
|
||||
const fetchBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
||||
type: BOOKMARKED_STATUSES_FETCH_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const fetchBookmarkedStatusesFail = (error: unknown, folderId?: string) => ({
|
||||
const fetchBookmarkedStatusesFail = (error: unknown) => ({
|
||||
type: BOOKMARKED_STATUSES_FETCH_FAIL,
|
||||
error,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const expandBookmarkedStatuses = (folderId?: string) =>
|
||||
const expandBookmarkedStatuses = () =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const list = folderId ? `bookmarks:${folderId}` : 'bookmarks';
|
||||
const url = getState().status_lists.get(list)?.next || null;
|
||||
const bookmarks = 'bookmarks';
|
||||
const url = getState().status_lists.get(bookmarks)?.next || null;
|
||||
|
||||
if (url === null || getState().status_lists.get(list)?.isLoading) {
|
||||
if (url === null || getState().status_lists.get(bookmarks)?.isLoading) {
|
||||
return dispatch(noOp);
|
||||
}
|
||||
|
||||
dispatch(expandBookmarkedStatusesRequest(folderId));
|
||||
dispatch(expandBookmarkedStatusesRequest());
|
||||
|
||||
return api(getState).get(url).then(response => {
|
||||
const next = getLinks(response).refs.find(link => link.rel === 'next');
|
||||
dispatch(importFetchedStatuses(response.data));
|
||||
return dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null, folderId));
|
||||
return dispatch(expandBookmarkedStatusesSuccess(response.data, next ? next.uri : null));
|
||||
}).catch(error => {
|
||||
dispatch(expandBookmarkedStatusesFail(error, folderId));
|
||||
dispatch(expandBookmarkedStatusesFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const expandBookmarkedStatusesRequest = (folderId?: string) => ({
|
||||
const expandBookmarkedStatusesRequest = () => ({
|
||||
type: BOOKMARKED_STATUSES_EXPAND_REQUEST,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const expandBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null, folderId?: string) => ({
|
||||
const expandBookmarkedStatusesSuccess = (statuses: APIEntity[], next: string | null) => ({
|
||||
type: BOOKMARKED_STATUSES_EXPAND_SUCCESS,
|
||||
statuses,
|
||||
next,
|
||||
folderId,
|
||||
});
|
||||
|
||||
const expandBookmarkedStatusesFail = (error: unknown, folderId?: string) => ({
|
||||
const expandBookmarkedStatusesFail = (error: unknown) => ({
|
||||
type: BOOKMARKED_STATUSES_EXPAND_FAIL,
|
||||
error,
|
||||
folderId,
|
||||
});
|
||||
|
||||
export {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import toast, { type IToastOptions } from 'soapbox/toast.tsx';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
import { isLoggedIn } from 'soapbox/utils/auth.ts';
|
||||
import { getFeatures } from 'soapbox/utils/features.ts';
|
||||
|
||||
import api, { getLinks } from '../api/index.ts';
|
||||
|
||||
import { fetchRelationships } from './accounts.ts';
|
||||
import { importFetchedAccounts, importFetchedStatus } from './importer/index.ts';
|
||||
import { openModal } from './modals.ts';
|
||||
import { expandGroupFeaturedTimeline } from './timelines.ts';
|
||||
|
||||
import type { AppDispatch, RootState } from 'soapbox/store.ts';
|
||||
|
@ -94,9 +92,7 @@ const ZAPS_EXPAND_FAIL = 'ZAPS_EXPAND_FAIL';
|
|||
const messages = defineMessages({
|
||||
bookmarkAdded: { id: 'status.bookmarked', defaultMessage: 'Bookmark added.' },
|
||||
bookmarkRemoved: { id: 'status.unbookmarked', defaultMessage: 'Bookmark removed.' },
|
||||
folderChanged: { id: 'status.bookmark_folder_changed', defaultMessage: 'Changed folder' },
|
||||
view: { id: 'toast.view', defaultMessage: 'View' },
|
||||
selectFolder: { id: 'status.bookmark.select_folder', defaultMessage: 'Select folder' },
|
||||
});
|
||||
|
||||
const reblog = (status: StatusEntity) =>
|
||||
|
@ -363,35 +359,17 @@ const zapFail = (status: StatusEntity, error: unknown) => ({
|
|||
skipLoading: true,
|
||||
});
|
||||
|
||||
const bookmark = (status: StatusEntity, folderId?: string) =>
|
||||
const bookmark = (status: StatusEntity) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
|
||||
const instance = state.instance;
|
||||
const features = getFeatures(instance);
|
||||
|
||||
dispatch(bookmarkRequest(status));
|
||||
|
||||
return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`, {
|
||||
folder_id: folderId,
|
||||
}).then(function(response) {
|
||||
return api(getState).post(`/api/v1/statuses/${status.id}/bookmark`).then(function(response) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(bookmarkSuccess(status, response.data));
|
||||
|
||||
let opts: IToastOptions = {
|
||||
actionLabel: messages.view,
|
||||
actionLink: folderId ? `/bookmarks/${folderId}` : '/bookmarks/all',
|
||||
};
|
||||
if (features.bookmarkFolders && typeof folderId !== 'string') {
|
||||
opts = {
|
||||
actionLabel: messages.selectFolder,
|
||||
action: () => dispatch(openModal('SELECT_BOOKMARK_FOLDER', {
|
||||
statusId: status.id,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
toast.success(typeof folderId === 'string' ? messages.folderChanged : messages.bookmarkAdded, opts);
|
||||
toast.success(messages.bookmarkAdded, {
|
||||
actionLink: '/bookmarks',
|
||||
});
|
||||
}).catch(function(error) {
|
||||
dispatch(bookmarkFail(status, error));
|
||||
});
|
||||
|
|
|
@ -43,13 +43,6 @@ export { useUnmuteGroup } from './groups/useUnmuteGroup.ts';
|
|||
export { useUpdateGroup } from './groups/useUpdateGroup.ts';
|
||||
export { useUpdateGroupTag } from './groups/useUpdateGroupTag.ts';
|
||||
|
||||
// Statuses
|
||||
export { useBookmarkFolders } from './statuses/useBookmarkFolders.ts';
|
||||
export { useBookmarkFolder } from './statuses/useBookmarkFolder.ts';
|
||||
export { useCreateBookmarkFolder } from './statuses/useCreateBookmarkFolder.ts';
|
||||
export { useDeleteBookmarkFolder } from './statuses/useDeleteBookmarkFolder.ts';
|
||||
export { useUpdateBookmarkFolder } from './statuses/useUpdateBookmarkFolder.ts';
|
||||
|
||||
// Streaming
|
||||
export { useUserStream } from './streaming/useUserStream.ts';
|
||||
export { useCommunityStream } from './streaming/useCommunityStream.ts';
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { selectEntity } from 'soapbox/entity-store/selectors.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { type BookmarkFolder } from 'soapbox/schemas/bookmark-folder.ts';
|
||||
|
||||
import { useBookmarkFolders } from './useBookmarkFolders.ts';
|
||||
|
||||
function useBookmarkFolder(folderId?: string) {
|
||||
const {
|
||||
isError,
|
||||
isFetched,
|
||||
isFetching,
|
||||
isLoading,
|
||||
invalidate,
|
||||
} = useBookmarkFolders();
|
||||
|
||||
const bookmarkFolder = useAppSelector(state => folderId
|
||||
? selectEntity<BookmarkFolder>(state, Entities.BOOKMARK_FOLDERS, folderId)
|
||||
: undefined);
|
||||
|
||||
return {
|
||||
bookmarkFolder,
|
||||
isError,
|
||||
isFetched,
|
||||
isFetching,
|
||||
isLoading,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
||||
export { useBookmarkFolder };
|
|
@ -1,25 +0,0 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { useEntities } from 'soapbox/entity-store/hooks/index.ts';
|
||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
import { bookmarkFolderSchema, type BookmarkFolder } from 'soapbox/schemas/bookmark-folder.ts';
|
||||
|
||||
function useBookmarkFolders() {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
const { entities, ...result } = useEntities<BookmarkFolder>(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
() => api.get('/api/v1/pleroma/bookmark_folders'),
|
||||
{ enabled: features.bookmarkFolders, schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
const bookmarkFolders = entities;
|
||||
|
||||
return {
|
||||
...result,
|
||||
bookmarkFolders,
|
||||
};
|
||||
}
|
||||
|
||||
export { useBookmarkFolders };
|
|
@ -1,31 +0,0 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts';
|
||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||
import { bookmarkFolderSchema } from 'soapbox/schemas/bookmark-folder.ts';
|
||||
|
||||
interface CreateBookmarkFolderParams {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
function useCreateBookmarkFolder() {
|
||||
const api = useApi();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
(params: CreateBookmarkFolderParams) =>
|
||||
api.post('/api/v1/pleroma/bookmark_folders', params, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
}),
|
||||
{ schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
createBookmarkFolder: createEntity,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
export { useCreateBookmarkFolder };
|
|
@ -1,16 +0,0 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { useEntityActions } from 'soapbox/entity-store/hooks/index.ts';
|
||||
|
||||
function useDeleteBookmarkFolder() {
|
||||
const { deleteEntity, isSubmitting } = useEntityActions(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
{ delete: '/api/v1/pleroma/bookmark_folders/:id' },
|
||||
);
|
||||
|
||||
return {
|
||||
deleteBookmarkFolder: deleteEntity,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
export { useDeleteBookmarkFolder };
|
|
@ -1,26 +0,0 @@
|
|||
import { Entities } from 'soapbox/entity-store/entities.ts';
|
||||
import { useCreateEntity } from 'soapbox/entity-store/hooks/index.ts';
|
||||
import { useApi } from 'soapbox/hooks/useApi.ts';
|
||||
import { bookmarkFolderSchema } from 'soapbox/schemas/bookmark-folder.ts';
|
||||
|
||||
interface UpdateBookmarkFolderParams {
|
||||
name: string;
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
function useUpdateBookmarkFolder(folderId: string) {
|
||||
const api = useApi();
|
||||
|
||||
const { createEntity, ...rest } = useCreateEntity(
|
||||
[Entities.BOOKMARK_FOLDERS],
|
||||
(params: UpdateBookmarkFolderParams) => api.patch(`/api/v1/pleroma/bookmark_folders/${folderId}`, params),
|
||||
{ schema: bookmarkFolderSchema },
|
||||
);
|
||||
|
||||
return {
|
||||
updateBookmarkFolder: createEntity,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
export { useUpdateBookmarkFolder };
|
|
@ -12,7 +12,6 @@ import dotsIcon from '@tabler/icons/outline/dots.svg';
|
|||
import editIcon from '@tabler/icons/outline/edit.svg';
|
||||
import externalLinkIcon from '@tabler/icons/outline/external-link.svg';
|
||||
import flagIcon from '@tabler/icons/outline/flag.svg';
|
||||
import foldersIcon from '@tabler/icons/outline/folders.svg';
|
||||
import gavelIcon from '@tabler/icons/outline/gavel.svg';
|
||||
import heartIcon from '@tabler/icons/outline/heart.svg';
|
||||
import lockIcon from '@tabler/icons/outline/lock.svg';
|
||||
|
@ -74,8 +73,6 @@ const messages = defineMessages({
|
|||
blockAndReport: { id: 'confirmations.block.block_and_report', defaultMessage: 'Block & Report' },
|
||||
blockConfirm: { id: 'confirmations.block.confirm', defaultMessage: 'Block' },
|
||||
bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' },
|
||||
bookmarkSetFolder: { id: 'status.bookmark_folder', defaultMessage: 'Set bookmark folder' },
|
||||
bookmarkChangeFolder: { id: 'status.bookmark_folder_change', defaultMessage: 'Change bookmark folder' },
|
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' },
|
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' },
|
||||
chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' },
|
||||
|
@ -239,12 +236,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
||||
const handleBookmarkFolderClick = () => {
|
||||
dispatch(openModal('SELECT_BOOKMARK_FOLDER', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
if (me) {
|
||||
const modalReblog = () => dispatch(toggleReblog(status));
|
||||
|
@ -501,14 +492,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (features.bookmarkFolders && fromBookmarks) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(status.pleroma.get('bookmark_folder') ? messages.bookmarkChangeFolder : messages.bookmarkSetFolder),
|
||||
action: handleBookmarkFolderClick,
|
||||
icon: foldersIcon,
|
||||
});
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
|
||||
menu.push({
|
||||
|
|
|
@ -2,7 +2,6 @@ import type * as Schemas from 'soapbox/schemas/index.ts';
|
|||
|
||||
enum Entities {
|
||||
ACCOUNTS = 'Accounts',
|
||||
BOOKMARK_FOLDERS = 'BookmarkFolders',
|
||||
DOMAINS = 'Domains',
|
||||
GROUPS = 'Groups',
|
||||
GROUP_MEMBERSHIPS = 'GroupMemberships',
|
||||
|
@ -18,7 +17,6 @@ enum Entities {
|
|||
|
||||
interface EntityTypes {
|
||||
[Entities.ACCOUNTS]: Schemas.Account;
|
||||
[Entities.BOOKMARK_FOLDERS]: Schemas.BookmarkFolder;
|
||||
[Entities.DOMAINS]: Schemas.Domain;
|
||||
[Entities.GROUPS]: Schemas.Group;
|
||||
[Entities.GROUP_MEMBERSHIPS]: Schemas.GroupMember;
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { useCreateBookmarkFolder } from 'soapbox/api/hooks/index.ts';
|
||||
import Button from 'soapbox/components/ui/button.tsx';
|
||||
import Form from 'soapbox/components/ui/form.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Input from 'soapbox/components/ui/input.tsx';
|
||||
import { useTextField } from 'soapbox/hooks/forms/index.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'bookmark_folders.new.title_placeholder', defaultMessage: 'New folder title' },
|
||||
createSuccess: { id: 'bookmark_folders.add.success', defaultMessage: 'Bookmark folder created successfully' },
|
||||
createFail: { id: 'bookmark_folders.add.fail', defaultMessage: 'Failed to create bookmark folder' },
|
||||
});
|
||||
|
||||
const NewFolderForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const name = useTextField();
|
||||
|
||||
const { createBookmarkFolder, isSubmitting } = useCreateBookmarkFolder();
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<Element>) => {
|
||||
e.preventDefault();
|
||||
createBookmarkFolder({
|
||||
name: name.value,
|
||||
}, {
|
||||
onSuccess() {
|
||||
toast.success(messages.createSuccess);
|
||||
},
|
||||
onError() {
|
||||
toast.success(messages.createFail);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<label className='grow'>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={label}
|
||||
disabled={isSubmitting}
|
||||
{...name}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
theme='primary'
|
||||
>
|
||||
<FormattedMessage id='bookmark_folders.new.create_title' defaultMessage='Add folder' />
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewFolderForm;
|
|
@ -1,82 +0,0 @@
|
|||
import bookmarksIcon from '@tabler/icons/outline/bookmarks.svg';
|
||||
import folderIcon from '@tabler/icons/outline/folder.svg';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { useBookmarkFolders } from 'soapbox/api/hooks/index.ts';
|
||||
import List, { ListItem } from 'soapbox/components/list.tsx';
|
||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||
import Emoji from 'soapbox/components/ui/emoji.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import { useFeatures } from 'soapbox/hooks/useFeatures.ts';
|
||||
|
||||
import NewFolderForm from './components/new-folder-form.tsx';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
});
|
||||
|
||||
const BookmarkFolders: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
|
||||
const { bookmarkFolders, isFetching } = useBookmarkFolders();
|
||||
|
||||
if (!features.bookmarkFolders) return <Redirect to='/bookmarks/all' />;
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Stack space={4}>
|
||||
<NewFolderForm />
|
||||
|
||||
<List>
|
||||
<ListItem
|
||||
to='/bookmarks/all'
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={bookmarksIcon} size={20} />
|
||||
<span><FormattedMessage id='bookmark_folders.all_bookmarks' defaultMessage='All bookmarks' /></span>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
{bookmarkFolders?.map((folder) => {
|
||||
let icon = <Icon src={folderIcon} size={20} />;
|
||||
|
||||
if (folder.emoji_url) {
|
||||
icon = <img src={folder.emoji_url} alt={folder.emoji} className='size-5' />;
|
||||
} else if (folder.emoji) {
|
||||
icon = <Emoji size={20} emoji={folder.emoji} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={folder.id}
|
||||
to={`/bookmarks/${folder.id}`}
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className='flex-none'>{icon}</div>
|
||||
<span>{folder.name}</span>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookmarkFolders;
|
|
@ -1,16 +1,9 @@
|
|||
import dotsVerticalIcon from '@tabler/icons/outline/dots-vertical.svg';
|
||||
import editIcon from '@tabler/icons/outline/edit.svg';
|
||||
import trashIcon from '@tabler/icons/outline/trash.svg';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchBookmarkedStatuses, expandBookmarkedStatuses } from 'soapbox/actions/bookmarks.ts';
|
||||
import { openModal } from 'soapbox/actions/modals.ts';
|
||||
import { useBookmarkFolder, useDeleteBookmarkFolder } from 'soapbox/api/hooks/index.ts';
|
||||
import DropdownMenu from 'soapbox/components/dropdown-menu/index.ts';
|
||||
import PullToRefresh from 'soapbox/components/pull-to-refresh.tsx';
|
||||
import StatusList from 'soapbox/components/status-list.tsx';
|
||||
import { Column } from 'soapbox/components/ui/column.tsx';
|
||||
|
@ -18,21 +11,13 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
|||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { useIsMobile } from 'soapbox/hooks/useIsMobile.ts';
|
||||
import { useTheme } from 'soapbox/hooks/useTheme.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
editFolder: { id: 'bookmarks.edit_folder', defaultMessage: 'Edit folder' },
|
||||
deleteFolder: { id: 'bookmarks.delete_folder', defaultMessage: 'Delete folder' },
|
||||
deleteFolderHeading: { id: 'confirmations.delete_bookmark_folder.heading', defaultMessage: 'Delete "{name}" folder?' },
|
||||
deleteFolderMessage: { id: 'confirmations.delete_bookmark_folder.message', defaultMessage: 'Are you sure you want to delete the folder? The bookmarks will still be stored.' },
|
||||
deleteFolderConfirm: { id: 'confirmations.delete_bookmark_folder.confirm', defaultMessage: 'Delete folder' },
|
||||
deleteFolderSuccess: { id: 'bookmarks.delete_folder.success', defaultMessage: 'Folder deleted' },
|
||||
deleteFolderFail: { id: 'bookmarks.delete_folder.fail', defaultMessage: 'Failed to delete folder' },
|
||||
});
|
||||
|
||||
const handleLoadMore = debounce((dispatch, folderId) => {
|
||||
dispatch(expandBookmarkedStatuses(folderId));
|
||||
const handleLoadMore = debounce((dispatch) => {
|
||||
dispatch(expandBookmarkedStatuses());
|
||||
}, 300, { leading: true });
|
||||
|
||||
interface IBookmarks {
|
||||
|
@ -42,79 +27,30 @@ interface IBookmarks {
|
|||
}
|
||||
|
||||
const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const theme = useTheme();
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const folderId = params?.id;
|
||||
const bookmarks = 'bookmarks';
|
||||
|
||||
const { bookmarkFolder: folder } = useBookmarkFolder(folderId);
|
||||
const { deleteBookmarkFolder } = useDeleteBookmarkFolder();
|
||||
|
||||
const bookmarksKey = folderId ? `bookmarks:${folderId}` : 'bookmarks';
|
||||
|
||||
const statusIds = useAppSelector((state) => state.status_lists.get(bookmarksKey)?.items || ImmutableOrderedSet<string>());
|
||||
const isLoading = useAppSelector((state) => state.status_lists.get(bookmarksKey)?.isLoading === true);
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists.get(bookmarksKey)?.next);
|
||||
const statusIds = useAppSelector((state) => state.status_lists.get(bookmarks)?.items || ImmutableOrderedSet<string>());
|
||||
const isLoading = useAppSelector((state) => state.status_lists.get(bookmarks)?.isLoading === true);
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists.get(bookmarks)?.next);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBookmarkedStatuses(folderId));
|
||||
}, [folderId]);
|
||||
dispatch(fetchBookmarkedStatuses());
|
||||
}, []);
|
||||
|
||||
const handleRefresh = () => {
|
||||
return dispatch(fetchBookmarkedStatuses(folderId));
|
||||
return dispatch(fetchBookmarkedStatuses());
|
||||
};
|
||||
|
||||
const handleEditFolder = () => {
|
||||
dispatch(openModal('EDIT_BOOKMARK_FOLDER', { folderId }));
|
||||
};
|
||||
|
||||
const handleDeleteFolder = () => {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
heading: intl.formatMessage(messages.deleteFolderHeading, { name: folder?.name }),
|
||||
message: intl.formatMessage(messages.deleteFolderMessage),
|
||||
confirm: intl.formatMessage(messages.deleteFolderConfirm),
|
||||
onConfirm: () => {
|
||||
deleteBookmarkFolder(folderId!, {
|
||||
onSuccess() {
|
||||
toast.success(messages.deleteFolderSuccess);
|
||||
history.push('/bookmarks');
|
||||
},
|
||||
onError() {
|
||||
toast.error(messages.deleteFolderFail);
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const emptyMessage = folderId
|
||||
? <FormattedMessage id='empty_column.bookmarks.folder' defaultMessage="You don't have any bookmarks in this folder yet. When you add one, it will show up here." />
|
||||
: <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||
|
||||
const items = folderId ? [
|
||||
{
|
||||
text: intl.formatMessage(messages.editFolder),
|
||||
action: handleEditFolder,
|
||||
icon: editIcon,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(messages.deleteFolder),
|
||||
action: handleDeleteFolder,
|
||||
icon: trashIcon,
|
||||
},
|
||||
] : [];
|
||||
const emptyMessage = <FormattedMessage id='empty_column.bookmarks' defaultMessage="You don't have any bookmarks yet. When you add one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column
|
||||
label={folder ? folder.name : intl.formatMessage(messages.heading)}
|
||||
action={
|
||||
<DropdownMenu items={items} src={dotsVerticalIcon} />
|
||||
}
|
||||
transparent={!isMobile}
|
||||
>
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent>
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<StatusList
|
||||
className='black:p-4 black:sm:p-5'
|
||||
|
@ -122,12 +58,13 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
|
|||
scrollKey='bookmarked_statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
|
||||
onLoadMore={() => handleLoadMore(dispatch, folderId)}
|
||||
onLoadMore={() => handleLoadMore(dispatch)}
|
||||
emptyMessage={emptyMessage}
|
||||
divideType={(theme === 'black' || isMobile) ? 'border' : 'space'}
|
||||
/>
|
||||
</PullToRefresh>
|
||||
</Column>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
CryptoDonateModal,
|
||||
DislikesModal,
|
||||
EditAnnouncementModal,
|
||||
EditBookmarkFolderModal,
|
||||
EditDomainModal,
|
||||
EditFederationModal,
|
||||
EmbedModal,
|
||||
|
@ -40,7 +39,6 @@ import {
|
|||
ReblogsModal,
|
||||
ReplyMentionsModal,
|
||||
ReportModal,
|
||||
SelectBookmarkFolderModal,
|
||||
UnauthorizedModal,
|
||||
VideoModal,
|
||||
EditRuleModal,
|
||||
|
@ -69,7 +67,6 @@ const MODAL_COMPONENTS: Record<string, React.ExoticComponent<any>> = {
|
|||
'CRYPTO_DONATE': CryptoDonateModal,
|
||||
'DISLIKES': DislikesModal,
|
||||
'EDIT_ANNOUNCEMENT': EditAnnouncementModal,
|
||||
'EDIT_BOOKMARK_FOLDER': EditBookmarkFolderModal,
|
||||
'EDIT_DOMAIN': EditDomainModal,
|
||||
'EDIT_FEDERATION': EditFederationModal,
|
||||
'EDIT_RULE': EditRuleModal,
|
||||
|
@ -95,7 +92,6 @@ const MODAL_COMPONENTS: Record<string, React.ExoticComponent<any>> = {
|
|||
'REBLOGS': ReblogsModal,
|
||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||
'REPORT': ReportModal,
|
||||
'SELECT_BOOKMARK_FOLDER': SelectBookmarkFolderModal,
|
||||
'UNAUTHORIZED': UnauthorizedModal,
|
||||
'VIDEO': VideoModal,
|
||||
'ZAPS': ZapsModal,
|
||||
|
|
|
@ -1,168 +0,0 @@
|
|||
import { useFloating, shift } from '@floating-ui/react';
|
||||
import moodHappyIcon from '@tabler/icons/outline/mood-happy.svg';
|
||||
import { useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { closeModal } from 'soapbox/actions/modals.ts';
|
||||
import { useBookmarkFolder, useUpdateBookmarkFolder } from 'soapbox/api/hooks/index.ts';
|
||||
import Emoji from 'soapbox/components/ui/emoji.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||
import Input from 'soapbox/components/ui/input.tsx';
|
||||
import Modal from 'soapbox/components/ui/modal.tsx';
|
||||
import EmojiPickerDropdown from 'soapbox/features/emoji/components/emoji-picker-dropdown.tsx';
|
||||
import { messages as emojiMessages } from 'soapbox/features/emoji/containers/emoji-picker-dropdown-container.tsx';
|
||||
import { useTextField } from 'soapbox/hooks/forms/index.ts';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useClickOutside } from 'soapbox/hooks/useClickOutside.ts';
|
||||
import toast from 'soapbox/toast.tsx';
|
||||
|
||||
import type { Emoji as EmojiType } from 'soapbox/features/emoji/index.ts';
|
||||
|
||||
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 size-[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 size={20} emoji={emoji} />
|
||||
: <Icon className='size-5 text-gray-600 hover:text-gray-700 dark:hover:text-gray-500' src={moodHappyIcon} />}
|
||||
</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,
|
||||
}, {
|
||||
onSuccess() {
|
||||
toast.success(intl.formatMessage(messages.editSuccess));
|
||||
dispatch(closeModal('EDIT_BOOKMARK_FOLDER'));
|
||||
},
|
||||
onError() {
|
||||
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;
|
|
@ -1,108 +0,0 @@
|
|||
import bookmarksIcon from '@tabler/icons/outline/bookmarks.svg';
|
||||
import folderIcon from '@tabler/icons/outline/folder.svg';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { bookmark } from 'soapbox/actions/interactions.ts';
|
||||
import { useBookmarkFolders } from 'soapbox/api/hooks/index.ts';
|
||||
import { RadioGroup, RadioItem } from 'soapbox/components/radio.tsx';
|
||||
import Emoji from 'soapbox/components/ui/emoji.tsx';
|
||||
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||
import Modal from 'soapbox/components/ui/modal.tsx';
|
||||
import Spinner from 'soapbox/components/ui/spinner.tsx';
|
||||
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||
import NewFolderForm from 'soapbox/features/bookmark-folders/components/new-folder-form.tsx';
|
||||
import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||
import { makeGetStatus } from 'soapbox/selectors/index.ts';
|
||||
|
||||
import type { Status as StatusEntity } from 'soapbox/types/entities.ts';
|
||||
|
||||
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={bookmarksIcon} 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) => {
|
||||
let icon = <Icon src={folderIcon} size={20} />;
|
||||
|
||||
if (folder.emoji_url) {
|
||||
icon = <img src={folder.emoji_url} alt={folder.emoji} className='size-5' />;
|
||||
} else if (folder.emoji) {
|
||||
icon = <Emoji size={20} emoji={folder.emoji} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RadioItem
|
||||
key={folder.id}
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<div className='flex-none'>{icon}</div>
|
||||
<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;
|
|
@ -141,7 +141,6 @@ import {
|
|||
RegisterInvite,
|
||||
ExternalLogin,
|
||||
LandingTimeline,
|
||||
BookmarkFolders,
|
||||
EditIdentity,
|
||||
Domains,
|
||||
NostrRelays,
|
||||
|
@ -259,9 +258,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
|
|||
|
||||
{features.lists && <WrappedRoute path='/lists' page={DefaultPage} component={Lists} content={children} />}
|
||||
{features.lists && <WrappedRoute path='/list/:id' page={DefaultPage} component={ListTimeline} content={children} />}
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks/all' page={DefaultPage} component={Bookmarks} content={children} />}
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks/:id' page={DefaultPage} component={Bookmarks} content={children} />}
|
||||
<WrappedRoute path='/bookmarks' page={DefaultPage} component={BookmarkFolders} content={children} />
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks' page={DefaultPage} component={Bookmarks} content={children} />}
|
||||
|
||||
<WrappedRoute path='/notifications' page={DefaultPage} component={Notifications} content={children} />
|
||||
|
||||
|
|
|
@ -164,9 +164,6 @@ export const ComposeEditor = lazy(() => import('soapbox/features/compose/editor/
|
|||
export const OnboardingFlowModal = lazy(() => import('soapbox/features/ui/components/modals/onboarding-flow-modal/onboarding-flow-modal.tsx'));
|
||||
export const NostrSignupModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-signup-modal/nostr-signup-modal.tsx'));
|
||||
export const NostrLoginModal = lazy(() => import('soapbox/features/ui/components/modals/nostr-login-modal/nostr-login-modal.tsx'));
|
||||
export const BookmarkFolders = lazy(() => import('soapbox/features/bookmark-folders/index.tsx'));
|
||||
export const EditBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/edit-bookmark-folder-modal.tsx'));
|
||||
export const SelectBookmarkFolderModal = lazy(() => import('soapbox/features/ui/components/modals/select-bookmark-folder-modal.tsx'));
|
||||
export const EditIdentity = lazy(() => import('soapbox/features/edit-identity/index.tsx'));
|
||||
export const Domains = lazy(() => import('soapbox/features/admin/domains.tsx'));
|
||||
export const EditDomainModal = lazy(() => import('soapbox/features/ui/components/modals/edit-domain-modal.tsx'));
|
||||
|
|
|
@ -233,17 +233,6 @@
|
|||
"badge_input.placeholder": "Enter a badge…",
|
||||
"birthday_panel.title": "Birthdays",
|
||||
"birthdays_modal.empty": "None of your friends have birthday today.",
|
||||
"bookmark_folders.add.fail": "Failed to create bookmark folder",
|
||||
"bookmark_folders.add.success": "Bookmark folder created successfully",
|
||||
"bookmark_folders.all_bookmarks": "All bookmarks",
|
||||
"bookmark_folders.edit.fail": "Failed to edit bookmark folder",
|
||||
"bookmark_folders.edit.success": "Bookmark folder edited successfully",
|
||||
"bookmark_folders.new.create_title": "Add folder",
|
||||
"bookmark_folders.new.title_placeholder": "New folder title",
|
||||
"bookmarks.delete_folder": "Delete folder",
|
||||
"bookmarks.delete_folder.fail": "Failed to delete folder",
|
||||
"bookmarks.delete_folder.success": "Folder deleted",
|
||||
"bookmarks.edit_folder": "Edit folder",
|
||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||
"boost_modal.title": "Repost?",
|
||||
"bundle_column_error.body": "Something went wrong while loading this page.",
|
||||
|
@ -559,9 +548,6 @@
|
|||
"confirmations.delete.confirm": "Delete",
|
||||
"confirmations.delete.heading": "Delete post",
|
||||
"confirmations.delete.message": "Are you sure you want to delete this post?",
|
||||
"confirmations.delete_bookmark_folder.confirm": "Delete folder",
|
||||
"confirmations.delete_bookmark_folder.heading": "Delete \"{name}\" folder?",
|
||||
"confirmations.delete_bookmark_folder.message": "Are you sure you want to delete the folder? The bookmarks will still be stored.",
|
||||
"confirmations.delete_event.confirm": "Delete",
|
||||
"confirmations.delete_event.heading": "Delete event",
|
||||
"confirmations.delete_event.message": "Are you sure you want to delete this event?",
|
||||
|
@ -645,8 +631,6 @@
|
|||
"directory.local": "From {domain} only",
|
||||
"directory.new_arrivals": "New arrivals",
|
||||
"directory.recently_active": "Recently active",
|
||||
"edit_bookmark_folder_modal.confirm": "Save",
|
||||
"edit_bookmark_folder_modal.header_title": "Edit folder",
|
||||
"edit_email.header": "Change Email",
|
||||
"edit_email.placeholder": "me@example.com",
|
||||
"edit_federation.followers_only": "Hide posts except to followers",
|
||||
|
@ -735,7 +719,6 @@
|
|||
"empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.",
|
||||
"empty_column.blocks": "You haven't blocked any users yet.",
|
||||
"empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.",
|
||||
"empty_column.bookmarks.folder": "You don't have any bookmarks in this folder yet. When you add one, it will show up here.",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||
"empty_column.dislikes": "No one has disliked this post yet. When someone does, they will show up here.",
|
||||
|
@ -1463,7 +1446,6 @@
|
|||
"security.update_email.success": "Email successfully updated.",
|
||||
"security.update_password.fail": "Update password failed.",
|
||||
"security.update_password.success": "Password successfully updated.",
|
||||
"select_bookmark_folder_modal.header_title": "Select folder",
|
||||
"settings.account_migration": "Move Account",
|
||||
"settings.blocks": "Blocks",
|
||||
"settings.change_email": "Change Email",
|
||||
|
@ -1540,10 +1522,6 @@
|
|||
"status.approval.pending": "Pending approval",
|
||||
"status.approval.rejected": "Rejected",
|
||||
"status.bookmark": "Bookmark",
|
||||
"status.bookmark.select_folder": "Select folder",
|
||||
"status.bookmark_folder": "Set bookmark folder",
|
||||
"status.bookmark_folder_change": "Change bookmark folder",
|
||||
"status.bookmark_folder_changed": "Changed folder",
|
||||
"status.bookmarked": "Bookmark added.",
|
||||
"status.cancel_reblog_private": "Un-repost",
|
||||
"status.cannot_reblog": "This post cannot be reposted",
|
||||
|
|
|
@ -67,7 +67,7 @@ import {
|
|||
} from '../actions/scheduled-statuses.ts';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
import type { APIEntity, Status as StatusEntity } from 'soapbox/types/entities.ts';
|
||||
import type { APIEntity } from 'soapbox/types/entities.ts';
|
||||
|
||||
export const StatusListRecord = ImmutableRecord({
|
||||
next: null as string | null,
|
||||
|
@ -94,8 +94,9 @@ const getStatusIds = (statuses: APIEntity[] = []) => (
|
|||
ImmutableOrderedSet(statuses.map(getStatusId))
|
||||
);
|
||||
|
||||
const setLoading = (state: State, listType: string, loading: boolean) =>
|
||||
state.update(listType, StatusListRecord(), listMap => listMap.set('isLoading', loading));
|
||||
const setLoading = (state: State, listType: string, loading: boolean) => {
|
||||
return state.update(listType, StatusListRecord(), listMap => listMap.set('isLoading', loading));
|
||||
};
|
||||
|
||||
const normalizeList = (state: State, listType: string, statuses: APIEntity[], next: string | null) => {
|
||||
return state.update(listType, StatusListRecord(), listMap => listMap.withMutations(map => {
|
||||
|
@ -133,24 +134,6 @@ const maybeAppendScheduledStatus = (state: State, status: APIEntity) => {
|
|||
return prependOneToList(state, 'scheduled_statuses', getStatusId(status));
|
||||
};
|
||||
|
||||
const addBookmarkToLists = (state: State, status: APIEntity) => {
|
||||
state = prependOneToList(state, 'bookmarks', status);
|
||||
const folderId = status.pleroma.bookmark_folder;
|
||||
if (folderId) {
|
||||
return prependOneToList(state, `bookmarks:${folderId}`, status);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
const removeBookmarkFromLists = (state: State, status: StatusEntity) => {
|
||||
state = removeOneFromList(state, 'bookmarks', status);
|
||||
const folderId = status.pleroma.get('bookmark_folder');
|
||||
if (folderId) {
|
||||
return removeOneFromList(state, `bookmarks:${folderId}`, status);
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default function statusLists(state = initialState, action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case FAVOURITED_STATUSES_FETCH_REQUEST:
|
||||
|
@ -175,22 +158,22 @@ export default function statusLists(state = initialState, action: AnyAction) {
|
|||
return appendToList(state, `favourites:${action.accountId}`, action.statuses, action.next);
|
||||
case BOOKMARKED_STATUSES_FETCH_REQUEST:
|
||||
case BOOKMARKED_STATUSES_EXPAND_REQUEST:
|
||||
return setLoading(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', true);
|
||||
return setLoading(state, 'bookmarks', true);
|
||||
case BOOKMARKED_STATUSES_FETCH_FAIL:
|
||||
case BOOKMARKED_STATUSES_EXPAND_FAIL:
|
||||
return setLoading(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', false);
|
||||
return setLoading(state, 'bookmarks', false);
|
||||
case BOOKMARKED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next);
|
||||
return normalizeList(state, 'bookmarks', action.statuses, action.next);
|
||||
case BOOKMARKED_STATUSES_EXPAND_SUCCESS:
|
||||
return appendToList(state, action.folderId ? `bookmarks:${action.folderId}` : 'bookmarks', action.statuses, action.next);
|
||||
return appendToList(state, 'bookmarks', action.statuses, action.next);
|
||||
case FAVOURITE_SUCCESS:
|
||||
return prependOneToList(state, 'favourites', action.status);
|
||||
case UNFAVOURITE_SUCCESS:
|
||||
return removeOneFromList(state, 'favourites', action.status);
|
||||
case BOOKMARK_SUCCESS:
|
||||
return addBookmarkToLists(state, action.response);
|
||||
return prependOneToList(state, 'bookmarks', action.response);
|
||||
case UNBOOKMARK_SUCCESS:
|
||||
return removeBookmarkFromLists(state, action.status);
|
||||
return removeOneFromList(state, 'bookmarks', action.status);
|
||||
case PINNED_STATUSES_FETCH_SUCCESS:
|
||||
return normalizeList(state, 'pins', action.statuses, action.next);
|
||||
case PIN_SUCCESS:
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
/** Pleroma bookmark folder. */
|
||||
const bookmarkFolderSchema = z.object({
|
||||
emoji: z.string().optional().catch(undefined),
|
||||
emoji_url: z.string().optional().catch(undefined),
|
||||
name: z.string().catch(''),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
type BookmarkFolder = z.infer<typeof bookmarkFolderSchema>;
|
||||
|
||||
export { bookmarkFolderSchema, type BookmarkFolder };
|
|
@ -2,7 +2,6 @@ export { accountSchema, type Account } from './account.ts';
|
|||
export { announcementSchema, adminAnnouncementSchema, type Announcement, type AdminAnnouncement } from './announcement.ts';
|
||||
export { announcementReactionSchema, type AnnouncementReaction } from './announcement-reaction.ts';
|
||||
export { attachmentSchema, type Attachment } from './attachment.ts';
|
||||
export { bookmarkFolderSchema, type BookmarkFolder } from './bookmark-folder.ts';
|
||||
export { cardSchema, type Card } from './card.ts';
|
||||
export { chatMessageSchema, type ChatMessage } from './chat-message.ts';
|
||||
export { customEmojiSchema, type CustomEmoji } from './custom-emoji.ts';
|
||||
|
|
|
@ -8,9 +8,9 @@ import { httpErrorMessages } from 'soapbox/utils/errors.ts';
|
|||
export type ToastText = string | MessageDescriptor
|
||||
export type ToastType = 'success' | 'error' | 'info'
|
||||
|
||||
export interface IToastOptions {
|
||||
interface IToastOptions {
|
||||
action?(): void;
|
||||
actionLink?: string;
|
||||
actionLink?: string; // not used in this file...
|
||||
actionLabel?: ToastText;
|
||||
duration?: number;
|
||||
summary?: string;
|
||||
|
|
|
@ -259,18 +259,10 @@ const getInstanceFeatures = (instance: InstanceV1 | InstanceV2) => {
|
|||
*/
|
||||
blocks: v.software !== DITTO,
|
||||
|
||||
/**
|
||||
* Can group bookmarks in folders.
|
||||
* @see GET /api/v1/pleroma/bookmark_folders
|
||||
* @see POST /api/v1/pleroma/bookmark_folders
|
||||
* @see PATCH /api/v1/pleroma/bookmark_folders/:id
|
||||
* @see DELETE /api/v1/pleroma/bookmark_folders/:id
|
||||
*/
|
||||
bookmarkFolders: features.includes('pleroma:bookmark_folders'),
|
||||
|
||||
/**
|
||||
* Can bookmark statuses.
|
||||
* @see POST /api/v1/statuses/:id/bookmark
|
||||
* @see POST /api/v1/statuses/:id/unbookmark
|
||||
* @see GET /api/v1/bookmarks
|
||||
*/
|
||||
bookmarks: any([
|
||||
|
|
Ładowanie…
Reference in New Issue