Merge branch 'remove-bookmark-folder' into 'main'

Remove bookmark folders

See merge request soapbox-pub/soapbox!3276
merge-requests/3281/head
Alex Gleason 2024-11-26 01:21:37 +00:00
commit edd4b692ef
24 zmienionych plików z 54 dodań i 795 usunięć

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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));
});

Wyświetl plik

@ -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';

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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({

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);
};

Wyświetl plik

@ -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,

Wyświetl plik

@ -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;

Wyświetl plik

@ -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;

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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'));

Wyświetl plik

@ -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",

Wyświetl plik

@ -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:

Wyświetl plik

@ -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 };

Wyświetl plik

@ -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';

Wyświetl plik

@ -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;

Wyświetl plik

@ -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([