From 95a67f0ba42a1bdb72084086e5e7f14e38652e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 21 Feb 2026 23:28:15 +0100 Subject: [PATCH 001/264] nicolium: Add note to the pl.mkljczk.pl deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .github/workflows/pl-fe.yaml | 2 +- packages/pl-fe/src/build-config.ts | 7 ++++--- .../src/features/compose/components/warning.tsx | 14 +++++++++++--- packages/pl-fe/src/layouts/home-layout.tsx | 10 ++++++++++ 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pl-fe.yaml b/.github/workflows/pl-fe.yaml index 31523cc79..a5ae393a2 100644 --- a/.github/workflows/pl-fe.yaml +++ b/.github/workflows/pl-fe.yaml @@ -79,7 +79,7 @@ jobs: NODE_ENV: production working-directory: ./packages/pl-fe run: | - WITH_LANDING_PAGE=true pnpm build + BANNER_HTML="pl.mkljczk.pl runs Nicolium's \`develop\` branch, which can break sometimes. For a more stable experience, use web.nicolium.app." WITH_LANDING_PAGE=true pnpm build cp dist/index.html dist/404.html cp pl-fe.zip dist/pl-fe.zip diff --git a/packages/pl-fe/src/build-config.ts b/packages/pl-fe/src/build-config.ts index 04deae652..7e2fa6a5f 100644 --- a/packages/pl-fe/src/build-config.ts +++ b/packages/pl-fe/src/build-config.ts @@ -4,7 +4,7 @@ */ const env = compileTime(() => { - const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE } = process.env; + const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE, BANNER_HTML } = process.env; const sanitizeURL = (url: string | undefined = ''): string => { try { @@ -22,11 +22,12 @@ const env = compileTime(() => { BACKEND_URL: sanitizeURL(BACKEND_URL), FE_SUBDIRECTORY: sanitizeBasename(FE_SUBDIRECTORY), WITH_LANDING_PAGE: WITH_LANDING_PAGE === 'true', + BANNER_HTML, }; }); -const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE } = env; +const { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE, BANNER_HTML } = env; export type PlFeEnv = typeof env; -export { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE }; +export { NODE_ENV, BACKEND_URL, FE_SUBDIRECTORY, WITH_LANDING_PAGE, BANNER_HTML }; diff --git a/packages/pl-fe/src/features/compose/components/warning.tsx b/packages/pl-fe/src/features/compose/components/warning.tsx index 6203b40cd..5b2c9497b 100644 --- a/packages/pl-fe/src/features/compose/components/warning.tsx +++ b/packages/pl-fe/src/features/compose/components/warning.tsx @@ -1,4 +1,5 @@ import { animated, useSpring } from '@react-spring/web'; +import clsx from 'clsx'; import React from 'react'; import { useSettings } from '@/stores/settings'; @@ -6,10 +7,15 @@ import { useSettings } from '@/stores/settings'; interface IWarning { message: React.ReactNode; animated?: boolean; + className?: string; } /** Warning message displayed in ComposeForm. */ -const Warning: React.FC = ({ message, animated: animate }) => { +const Warning: React.FC = ({ + message, + animated: animate, + className: customClassName, +}) => { const { reduceMotion } = useSettings(); const styles = useSpring({ @@ -24,8 +30,10 @@ const Warning: React.FC = ({ message, animated: animate }) => { immediate: !animate || reduceMotion, }); - const className = - 'rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white'; + const className = clsx( + 'rounded border border-solid border-gray-400 bg-transparent px-2.5 py-2 text-xs text-gray-900 dark:border-gray-800 dark:text-white', + customClassName, + ); if (!message) return null; diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index 82cf5a14d..471047759 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -4,8 +4,11 @@ import React, { useRef } from 'react'; import { useIntl } from 'react-intl'; import { uploadCompose } from '@/actions/compose'; +import { BANNER_HTML } from '@/build-config'; import Avatar from '@/components/ui/avatar'; import Layout from '@/components/ui/layout'; +import Text from '@/components/ui/text'; +import Warning from '@/features/compose/components/warning'; import LinkFooter from '@/features/ui/components/link-footer'; import { WhoToFollowPanel, @@ -90,6 +93,13 @@ const HomeLayout = () => { )} + {BANNER_HTML && BANNER_HTML.length > 0 && ( + } + className='my-4 sm:mx-4' + /> + )} + From f73d066bab02b098588a6f013b197b48b3fed95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 21 Feb 2026 23:38:02 +0100 Subject: [PATCH 002/264] nicolium: do not render parts of ui hidden behind breakpoints (fixes announcements display) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/ui/layout.tsx | 77 +++++++++++++++++---- 1 file changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/pl-fe/src/components/ui/layout.tsx b/packages/pl-fe/src/components/ui/layout.tsx index 9067af77f..c1c4b22a3 100644 --- a/packages/pl-fe/src/components/ui/layout.tsx +++ b/packages/pl-fe/src/components/ui/layout.tsx @@ -1,9 +1,16 @@ import clsx from 'clsx'; -import React, { Suspense } from 'react'; +import React, { Suspense, useEffect, useState } from 'react'; import StickyBox from 'react-sticky-box'; import { useFeatures } from '@/hooks/use-features'; +import tailwindConfig from '../../../tailwind.config'; + +const breakpoints = (tailwindConfig.theme?.screens as Record) ?? { + lg: '976px', + xl: '1280px', +}; + interface ISidebar { children: React.ReactNode; shrink?: boolean; @@ -23,6 +30,30 @@ interface LayoutComponent extends React.FC { Aside: React.FC; } +const useMinWidth = (query: string) => { + const getMatch = () => (typeof window !== 'undefined' ? window.matchMedia(query).matches : false); + + const [matches, setMatches] = useState(getMatch); + + useEffect(() => { + const mediaQuery = window.matchMedia(query); + + setMatches(mediaQuery.matches); + + const onChange = (event: MediaQueryListEvent) => { + setMatches(event.matches); + }; + + mediaQuery.addEventListener('change', onChange); + + return () => { + mediaQuery.removeEventListener('change', onChange); + }; + }, [query]); + + return matches; +}; + /** Layout container, to hold Sidebar, Main, and Aside. */ const Layout: LayoutComponent = ({ children, fullWidth }) => (
@@ -37,13 +68,21 @@ const Layout: LayoutComponent = ({ children, fullWidth }) => ( ); /** Left sidebar container in the UI. */ -const Sidebar: React.FC = ({ children, shrink }) => ( -
- - {children} - -
-); +const Sidebar: React.FC = ({ children, shrink }) => { + const isVisible = useMinWidth(`(min-width: ${breakpoints.lg})`); + + if (!isVisible) { + return null; + } + + return ( +
+ + {children} + +
+ ); +}; /** Center column container in the UI. */ const Main: React.FC> = ({ children, className }) => { @@ -65,13 +104,21 @@ const Main: React.FC> = ({ children, classN }; /** Right sidebar container in the UI. */ -const Aside: React.FC = ({ children }) => ( - -); +const Aside: React.FC = ({ children }) => { + const isVisible = useMinWidth(`(min-width: ${breakpoints.xl})`); + + if (!isVisible) { + return null; + } + + return ( + + ); +}; Layout.Sidebar = Sidebar; Layout.Main = Main; From d95ba3278cfeaff02e52c9e3d8d5d29ac3d21164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sat, 21 Feb 2026 23:43:35 +0100 Subject: [PATCH 003/264] nicolium: fix banner style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/layouts/home-layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index 471047759..78d9bfaae 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -96,7 +96,7 @@ const HomeLayout = () => { {BANNER_HTML && BANNER_HTML.length > 0 && ( } - className='my-4 sm:mx-4' + className='!m-4' /> )} From 77b763cade5c58d1126a69f3da5328a43cec4b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 13:12:20 +0100 Subject: [PATCH 004/264] nicolium: account scrobble query changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/components/account-hover-card.tsx | 5 ++--- .../components/panels/profile-info-panel.tsx | 5 ++--- .../src/queries/accounts/account-scrobble.ts | 19 ++++++++++++------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index ec730b81c..b82215233 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -1,5 +1,4 @@ import { autoUpdate, flip, shift, useFloating, useTransitionStyles } from '@floating-ui/react'; -import { useQuery } from '@tanstack/react-query'; import { useRouter } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect } from 'react'; @@ -18,7 +17,7 @@ import { isTimezoneLabel } from '@/features/ui/components/profile-field'; import { UserPanel } from '@/features/ui/util/async-components'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; -import { accountScrobbleQueryOptions } from '@/queries/accounts/account-scrobble'; +import { useAccountScrobbleQuery } from '@/queries/accounts/account-scrobble'; import { useAccountHoverCardActions, useAccountHoverCardStore } from '@/stores/account-hover-card'; import AccountLocalTime from './account-local-time'; @@ -79,7 +78,7 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { const me = useAppSelector((state) => state.me); const { account } = useAccount(accountId ?? undefined, { withRelationship: true }); - const { data: scrobble } = useQuery(accountScrobbleQueryOptions(account?.id)); + const { data: scrobble } = useAccountScrobbleQuery(account?.id); const badges = getBadges(account); useEffect(() => { diff --git a/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx index d647e610f..c8b556ca4 100644 --- a/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/profile-info-panel.tsx @@ -1,4 +1,3 @@ -import { useQuery } from '@tanstack/react-query'; import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; @@ -14,7 +13,7 @@ import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import { useAcct } from '@/hooks/use-acct'; import { useAppSelector } from '@/hooks/use-app-selector'; -import { accountScrobbleQueryOptions } from '@/queries/accounts/account-scrobble'; +import { useAccountScrobbleQuery } from '@/queries/accounts/account-scrobble'; import { capitalize } from '@/utils/strings'; import { ProfileField } from '../../util/async-components'; @@ -51,7 +50,7 @@ const ProfileInfoPanel: React.FC = ({ account, username }) => const me = useAppSelector((state) => state.me); const ownAccount = account?.id === me; - const { data: scrobble } = useQuery(accountScrobbleQueryOptions(account?.id)); + const { data: scrobble } = useAccountScrobbleQuery(account?.id); const getStaffBadge = (): React.ReactNode => { if (account?.is_admin) { diff --git a/packages/pl-fe/src/queries/accounts/account-scrobble.ts b/packages/pl-fe/src/queries/accounts/account-scrobble.ts index 1ce5d4ddf..9772b3e33 100644 --- a/packages/pl-fe/src/queries/accounts/account-scrobble.ts +++ b/packages/pl-fe/src/queries/accounts/account-scrobble.ts @@ -1,15 +1,20 @@ -import { queryOptions } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; -import { getClient } from '@/api'; +import { useClient } from '@/hooks/use-client'; +import { useFeatures } from '@/hooks/use-features'; -const accountScrobbleQueryOptions = (accountId?: string) => - queryOptions({ +const useAccountScrobbleQuery = (accountId?: string) => { + const client = useClient(); + const features = useFeatures(); + + return useQuery({ queryKey: ['scrobbles', accountId!], queryFn: async () => - (await getClient().accounts.getScrobbles(accountId!, { limit: 1 })).items[0] || null, + (await client.accounts.getScrobbles(accountId!, { limit: 1 })).items[0] || null, placeholderData: undefined, - enabled: () => !!accountId && getClient().features.scrobbles, + enabled: () => !!accountId && features.scrobbles, staleTime: 3 * 60 * 1000, }); +}; -export { accountScrobbleQueryOptions }; +export { useAccountScrobbleQuery }; From 23a3953ea1af286f3ddb3acac10e4f99b0511049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 13:28:59 +0100 Subject: [PATCH 005/264] nicolium: add current account context (currently unused) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/contexts/current-account-context.tsx | 23 +++++++++++++++++++ packages/pl-fe/src/init/pl-fe.tsx | 19 ++++++++------- 2 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 packages/pl-fe/src/contexts/current-account-context.tsx diff --git a/packages/pl-fe/src/contexts/current-account-context.tsx b/packages/pl-fe/src/contexts/current-account-context.tsx new file mode 100644 index 000000000..85be5cb0c --- /dev/null +++ b/packages/pl-fe/src/contexts/current-account-context.tsx @@ -0,0 +1,23 @@ +import React, { createContext, useContext } from 'react'; + +import { useAppSelector } from '@/hooks/use-app-selector'; + +const CurrentAccountContext = createContext<'unauthenticated' | string>('unauthenticated'); + +interface ICurrentAccountProvider { + children: React.ReactNode; +} + +const DefaultCurrentAccountProvider: React.FC = ({ children }) => { + const me = useAppSelector((state) => state.me); + + return ( + + {children} + + ); +}; + +const useCurrentAccount = () => useContext(CurrentAccountContext); + +export { CurrentAccountContext, DefaultCurrentAccountProvider, useCurrentAccount }; diff --git a/packages/pl-fe/src/init/pl-fe.tsx b/packages/pl-fe/src/init/pl-fe.tsx index 8eec13b80..f2abf8e8c 100644 --- a/packages/pl-fe/src/init/pl-fe.tsx +++ b/packages/pl-fe/src/init/pl-fe.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { HelmetProvider } from 'react-helmet-async'; import { Provider } from 'react-redux'; +import { DefaultCurrentAccountProvider } from '@/contexts/current-account-context'; import { StatProvider } from '@/contexts/stat-context'; import { queryClient } from '@/queries/client'; @@ -21,14 +22,16 @@ const PlFe: React.FC = () => ( <> - - - - - - - - + + + + + + + + + + From 9a3bcc163c445becda41b8b84384adcf6281fa09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 18:49:06 +0100 Subject: [PATCH 006/264] nicolium: what's this nesting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/quoted-status.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/pl-fe/src/components/quoted-status.tsx b/packages/pl-fe/src/components/quoted-status.tsx index a5265434f..aa6b9cffa 100644 --- a/packages/pl-fe/src/components/quoted-status.tsx +++ b/packages/pl-fe/src/components/quoted-status.tsx @@ -98,10 +98,8 @@ const QuotedStatus: React.FC = ({ status, onCancel, compose }) => {status.event ? ( ) : ( - - - - + + )} From be44696640a5c83d4edc7bc125fef83f74659ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 18:57:16 +0100 Subject: [PATCH 007/264] nicolium: add quote indicator that feels missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/compose/components/reply-indicator.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/pl-fe/src/features/compose/components/reply-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-indicator.tsx index df4fbba4a..daf367cdd 100644 --- a/packages/pl-fe/src/features/compose/components/reply-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-indicator.tsx @@ -4,6 +4,7 @@ import React from 'react'; import AttachmentThumbs from '@/components/attachment-thumbs'; import Markup from '@/components/markup'; import { ParsedContent } from '@/components/parsed-content'; +import QuotedStatusIndicator from '@/components/quoted-status-indicator'; import Stack from '@/components/ui/stack'; import AccountContainer from '@/containers/account-container'; import { getTextDirection } from '@/utils/rtl'; @@ -25,6 +26,7 @@ interface IReplyIndicator { | 'sensitive' | 'spoiler_text' | 'quote_id' + | 'quote_url' >; onCancel?: () => void; hideActions: boolean; @@ -81,6 +83,10 @@ const ReplyIndicator: React.FC = ({ {status.media_attachments.length > 0 && } + + {status.quote_id && ( + + )} ); }; From fbbcbdce3f3c2c2d7c69bf3371ba0962024b9ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 20:08:16 +0100 Subject: [PATCH 008/264] nicolium: groups migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/entities/group-member.ts | 1 + .../pl-api/lib/entities/group-relationship.ts | 2 +- packages/pl-fe/src/api/batcher.ts | 9 ++ .../hooks/groups/use-demote-group-member.ts | 27 ------ .../groups/use-group-membership-requests.ts | 54 ----------- .../hooks/groups/use-group-relationship.ts | 30 ------ .../hooks/groups/use-group-relationships.ts | 25 ----- .../pl-fe/src/api/hooks/groups/use-group.ts | 45 --------- .../pl-fe/src/api/hooks/groups/use-groups.ts | 35 ------- .../src/api/hooks/groups/use-join-group.ts | 25 ----- .../src/api/hooks/groups/use-leave-group.ts | 25 ----- .../hooks/groups/use-promote-group-member.ts | 27 ------ packages/pl-fe/src/components/group-card.tsx | 77 ++++++++------- .../src/components/status-action-bar.tsx | 13 ++- .../group/components/group-action-button.tsx | 75 ++++++--------- .../components/group-member-list-item.tsx | 13 +-- .../group/components/group-options-button.tsx | 9 +- .../components/discover/group-list-item.tsx | 21 ++-- .../features/ui/components/compose-button.tsx | 6 +- .../ui/components/panels/my-groups-panel.tsx | 12 ++- packages/pl-fe/src/layouts/group-layout.tsx | 12 +-- .../pl-fe/src/pages/groups/edit-group.tsx | 5 +- .../pages/groups/group-blocked-members.tsx | 4 +- .../pl-fe/src/pages/groups/group-gallery.tsx | 4 +- .../pl-fe/src/pages/groups/group-members.tsx | 20 ++-- .../groups/group-membership-requests.tsx | 87 +++++++++-------- packages/pl-fe/src/pages/groups/groups.tsx | 24 ++--- .../pl-fe/src/pages/groups/manage-group.tsx | 6 +- .../src/pages/timelines/group-timeline.tsx | 4 +- .../src/queries/groups/use-group-members.ts | 81 +++++++++++++++- .../queries/groups/use-group-relationship.ts | 96 +++++++++++++++++++ .../pl-fe/src/queries/groups/use-group.ts | 33 +++++++ .../pl-fe/src/queries/groups/use-groups.ts | 25 +++++ .../pl-fe/src/queries/utils/minify-list.ts | 13 +++ 34 files changed, 446 insertions(+), 499 deletions(-) delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-group-relationships.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-group.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-groups.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-join-group.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-leave-group.ts delete mode 100644 packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts create mode 100644 packages/pl-fe/src/queries/groups/use-group-relationship.ts create mode 100644 packages/pl-fe/src/queries/groups/use-group.ts create mode 100644 packages/pl-fe/src/queries/groups/use-groups.ts diff --git a/packages/pl-api/lib/entities/group-member.ts b/packages/pl-api/lib/entities/group-member.ts index 1ec0f99c2..256af9bcc 100644 --- a/packages/pl-api/lib/entities/group-member.ts +++ b/packages/pl-api/lib/entities/group-member.ts @@ -5,6 +5,7 @@ import { accountSchema } from './account'; enum GroupRoles { OWNER = 'owner', ADMIN = 'admin', + MODERATOR = 'moderator', USER = 'user', } diff --git a/packages/pl-api/lib/entities/group-relationship.ts b/packages/pl-api/lib/entities/group-relationship.ts index 2ef313133..8c8d1481e 100644 --- a/packages/pl-api/lib/entities/group-relationship.ts +++ b/packages/pl-api/lib/entities/group-relationship.ts @@ -8,7 +8,7 @@ import { GroupRoles } from './group-member'; const groupRelationshipSchema = v.object({ id: v.string(), member: v.fallback(v.boolean(), false), - role: v.fallback(v.enum(GroupRoles), GroupRoles.USER), + role: v.fallback(v.optional(v.enum(GroupRoles)), undefined), requested: v.fallback(v.boolean(), false), }); diff --git a/packages/pl-fe/src/api/batcher.ts b/packages/pl-fe/src/api/batcher.ts index 5a9482d87..01f930f23 100644 --- a/packages/pl-fe/src/api/batcher.ts +++ b/packages/pl-fe/src/api/batcher.ts @@ -11,6 +11,14 @@ const relationships = memoize((client: PlApiClient) => }), ); +const groupRelationships = memoize((client: PlApiClient) => + create({ + fetcher: (ids: string[]) => client.experimental.groups.getGroupRelationships(ids), + resolver: keyResolver('id'), + scheduler: bufferScheduler(200), + }), +); + // TODO: proper multi-client support const translations = memoize((lang: string, client: PlApiClient) => create({ @@ -22,6 +30,7 @@ const translations = memoize((lang: string, client: PlApiClient) => const batcher = { relationships, + groupRelationships, translations, }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts b/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts deleted file mode 100644 index 9e76e5686..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-demote-group-member.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as v from 'valibot'; - -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -import type { Group, GroupMember, GroupRole } from 'pl-api'; - -const useDemoteGroupMember = (group: Pick, groupMember: Pick) => { - const client = useClient(); - - const { createEntity } = useCreateEntity( - [Entities.GROUP_MEMBERSHIPS, groupMember.id], - ({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => - client.experimental.groups.demoteGroupUsers(group.id, account_ids, role), - { - schema: v.pipe( - v.any(), - v.transform((arr) => arr[0]), - ), - }, - ); - - return createEntity; -}; - -export { useDemoteGroupMember }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts b/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts deleted file mode 100644 index cc19ce139..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group-membership-requests.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { GroupRoles } from 'pl-api'; - -import { Entities } from '@/entity-store/entities'; -import { useDismissEntity } from '@/entity-store/hooks/use-dismiss-entity'; -import { useEntities } from '@/entity-store/hooks/use-entities'; -import { useClient } from '@/hooks/use-client'; - -import { useGroupRelationship } from './use-group-relationship'; - -import type { ExpandedEntitiesPath } from '@/entity-store/hooks/types'; -import type { Account } from 'pl-api'; - -const useGroupMembershipRequests = (groupId: string) => { - const client = useClient(); - const path: ExpandedEntitiesPath = [Entities.ACCOUNTS, 'membership_requests', groupId]; - - const { groupRelationship: relationship } = useGroupRelationship(groupId); - - const { entities, invalidate, fetchEntities, ...rest } = useEntities( - path, - () => client.experimental.groups.getGroupMembershipRequests(groupId), - { - enabled: relationship?.role === GroupRoles.OWNER || relationship?.role === GroupRoles.ADMIN, - }, - ); - - const { dismissEntity: authorize } = useDismissEntity(path, async (accountId: string) => { - const response = await client.experimental.groups.acceptGroupMembershipRequest( - groupId, - accountId, - ); - invalidate(); - return response; - }); - - const { dismissEntity: reject } = useDismissEntity(path, async (accountId: string) => { - const response = await client.experimental.groups.rejectGroupMembershipRequest( - groupId, - accountId, - ); - invalidate(); - return response; - }); - - return { - accounts: entities, - refetch: fetchEntities, - authorize, - reject, - ...rest, - }; -}; - -export { useGroupMembershipRequests }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts b/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts deleted file mode 100644 index dcff57403..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group-relationship.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as v from 'valibot'; - -import { Entities } from '@/entity-store/entities'; -import { useEntity } from '@/entity-store/hooks/use-entity'; -import { useClient } from '@/hooks/use-client'; - -import type { GroupRelationship } from 'pl-api'; - -const useGroupRelationship = (groupId: string | undefined) => { - const client = useClient(); - - const { entity: groupRelationship, ...result } = useEntity( - [Entities.GROUP_RELATIONSHIPS, groupId!], - () => client.experimental.groups.getGroupRelationships([groupId!]), - { - enabled: !!groupId, - schema: v.pipe( - v.any(), - v.transform((arr) => arr[0]), - ), - }, - ); - - return { - groupRelationship, - ...result, - }; -}; - -export { useGroupRelationship }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group-relationships.ts b/packages/pl-fe/src/api/hooks/groups/use-group-relationships.ts deleted file mode 100644 index 2d6375598..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group-relationships.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useBatchedEntities } from '@/entity-store/hooks/use-batched-entities'; -import { useClient } from '@/hooks/use-client'; -import { useLoggedIn } from '@/hooks/use-logged-in'; - -import type { GroupRelationship } from 'pl-api'; - -const useGroupRelationships = (listKey: string[], groupIds: string[]) => { - const client = useClient(); - const { isLoggedIn } = useLoggedIn(); - - const fetchGroupRelationships = (groupIds: string[]) => - client.experimental.groups.getGroupRelationships(groupIds); - - const { entityMap: relationships, ...result } = useBatchedEntities( - [Entities.GROUP_RELATIONSHIPS, ...listKey], - groupIds, - fetchGroupRelationships, - { enabled: isLoggedIn }, - ); - - return { relationships, ...result }; -}; - -export { useGroupRelationships }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-group.ts b/packages/pl-fe/src/api/hooks/groups/use-group.ts deleted file mode 100644 index 76646284d..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-group.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useLocation, useNavigate } from '@tanstack/react-router'; -import { useEffect } from 'react'; - -import { Entities } from '@/entity-store/entities'; -import { useEntity } from '@/entity-store/hooks/use-entity'; -import { useClient } from '@/hooks/use-client'; - -import { useGroupRelationship } from './use-group-relationship'; - -import type { Group } from 'pl-api'; - -const useGroup = (groupId: string, refetch = true) => { - const client = useClient(); - const location = useLocation(); - const navigate = useNavigate(); - - const { - entity: group, - isUnauthorized, - ...result - } = useEntity( - [Entities.GROUPS, groupId], - () => client.experimental.groups.getGroup(groupId), - { - refetch, - enabled: !!groupId, - }, - ); - const { groupRelationship: relationship } = useGroupRelationship(groupId); - - useEffect(() => { - if (isUnauthorized) { - localStorage.setItem('plfe:redirect_uri', location.href); - navigate({ to: '/login', replace: true }); - } - }, [isUnauthorized]); - - return { - ...result, - isUnauthorized, - group: group ? { ...group, relationship: relationship ?? null } : undefined, - }; -}; - -export { useGroup }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-groups.ts b/packages/pl-fe/src/api/hooks/groups/use-groups.ts deleted file mode 100644 index 291392912..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-groups.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useEntities } from '@/entity-store/hooks/use-entities'; -import { useClient } from '@/hooks/use-client'; -import { useFeatures } from '@/hooks/use-features'; - -import { useGroupRelationships } from './use-group-relationships'; - -import type { Group } from 'pl-api'; - -const useGroups = () => { - const client = useClient(); - const features = useFeatures(); - - const { entities, ...result } = useEntities( - [Entities.GROUPS, 'search', ''], - () => client.experimental.groups.getGroups(), - { enabled: features.groups }, - ); - const { relationships } = useGroupRelationships( - ['search', ''], - entities.map((entity) => entity.id), - ); - - const groups = entities.map((group) => ({ - ...group, - relationship: relationships[group.id] || null, - })); - - return { - ...result, - groups, - }; -}; - -export { useGroups }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-join-group.ts b/packages/pl-fe/src/api/hooks/groups/use-join-group.ts deleted file mode 100644 index 7b51cb6f3..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-join-group.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -import { useGroups } from './use-groups'; - -import type { Group } from 'pl-api'; - -const useJoinGroup = (group: Pick) => { - const client = useClient(); - const { invalidate } = useGroups(); - - const { createEntity, isSubmitting } = useCreateEntity( - [Entities.GROUP_RELATIONSHIPS, group.id], - () => client.experimental.groups.joinGroup(group.id), - ); - - return { - mutate: createEntity, - isSubmitting, - invalidate, - }; -}; - -export { useJoinGroup }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-leave-group.ts b/packages/pl-fe/src/api/hooks/groups/use-leave-group.ts deleted file mode 100644 index e60ab0bff..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-leave-group.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -import { useGroups } from './use-groups'; - -import type { Group } from 'pl-api'; - -const useLeaveGroup = (group: Pick) => { - const client = useClient(); - const { invalidate } = useGroups(); - - const { createEntity, isSubmitting } = useCreateEntity( - [Entities.GROUP_RELATIONSHIPS, group.id], - () => client.experimental.groups.leaveGroup(group.id), - ); - - return { - mutate: createEntity, - isSubmitting, - invalidate, - }; -}; - -export { useLeaveGroup }; diff --git a/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts b/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts deleted file mode 100644 index 2ad3c85ba..000000000 --- a/packages/pl-fe/src/api/hooks/groups/use-promote-group-member.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as v from 'valibot'; - -import { Entities } from '@/entity-store/entities'; -import { useCreateEntity } from '@/entity-store/hooks/use-create-entity'; -import { useClient } from '@/hooks/use-client'; - -import type { Group, GroupMember, GroupRole } from 'pl-api'; - -const usePromoteGroupMember = (group: Pick, groupMember: Pick) => { - const client = useClient(); - - const { createEntity } = useCreateEntity( - [Entities.GROUP_MEMBERSHIPS, groupMember.id], - ({ account_ids, role }: { account_ids: string[]; role: GroupRole }) => - client.experimental.groups.promoteGroupUsers(group.id, account_ids, role), - { - schema: v.pipe( - v.any(), - v.transform((arr) => arr[0]), - ), - }, - ); - - return createEntity; -}; - -export { usePromoteGroupMember }; diff --git a/packages/pl-fe/src/components/group-card.tsx b/packages/pl-fe/src/components/group-card.tsx index c21eea2c1..0d0c6b7f0 100644 --- a/packages/pl-fe/src/components/group-card.tsx +++ b/packages/pl-fe/src/components/group-card.tsx @@ -8,48 +8,53 @@ import GroupHeaderImage from '@/features/group/components/group-header-image'; import GroupMemberCount from '@/features/group/components/group-member-count'; import GroupPrivacy from '@/features/group/components/group-privacy'; import GroupRelationship from '@/features/group/components/group-relationship'; +import { useGroupQuery } from '@/queries/groups/use-group'; import GroupAvatar from './groups/group-avatar'; -import type { Group } from 'pl-api'; - interface IGroupCard { - group: Group; + groupId: string; } -const GroupCard: React.FC = ({ group }) => ( - - {/* Group Cover Image */} - - +const GroupCard: React.FC = ({ groupId }) => { + const { data: group } = useGroupQuery(groupId, true); + + if (!group) return null; + + return ( + + {/* Group Cover Image */} + + + + + {/* Group Avatar */} +
+ +
+ + {/* Group Info */} + + + + + + + + + + + + +
- - {/* Group Avatar */} -
- -
- - {/* Group Info */} - - - - - - - - - - - - - -
-); + ); +}; export { GroupCard as default }; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 18dd1fcdc..932bdc561 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -10,8 +10,6 @@ import { deleteStatusModal, toggleStatusSensitivityModal } from '@/actions/moder import { initReport, ReportableEntities } from '@/actions/reports'; import { changeSetting } from '@/actions/settings'; import { deleteStatus, editStatus, toggleMuteStatus } from '@/actions/statuses'; -import { useGroup } from '@/api/hooks/groups/use-group'; -import { useGroupRelationship } from '@/api/hooks/groups/use-group-relationship'; import DropdownMenu from '@/components/dropdown-menu'; import StatusActionButton from '@/components/status-action-button'; import EmojiPickerDropdown from '@/features/emoji/containers/emoji-picker-dropdown-container'; @@ -26,7 +24,9 @@ import { useInstance } from '@/hooks/use-instance'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useUnblockAccountMutation } from '@/queries/accounts/use-relationship'; import { useChats } from '@/queries/chats'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { useBlockGroupUserMutation } from '@/queries/groups/use-group-blocks'; +import { useGroupRelationshipQuery } from '@/queries/groups/use-group-relationship'; import { useCustomEmojis } from '@/queries/instance/use-custom-emojis'; import { useTranslationLanguages } from '@/queries/instance/use-translation-languages'; import { @@ -345,7 +345,7 @@ const ReplyButton: React.FC = ({ const intl = useIntl(); const canReply = useCanInteract(status, 'can_reply'); - const { groupRelationship } = useGroupRelationship(status.group_id ?? undefined); + const { data: groupRelationship } = useGroupRelationshipQuery(status.group_id ?? undefined); let replyTitle; let replyDisabled = false; @@ -739,7 +739,7 @@ const MenuButton: React.FC = ({ const { fetchTranslation, hideTranslation } = useStatusMetaActions(); const { targetLanguage } = useStatusMeta(status.id); const { openModal } = useModalsActions(); - const { group } = useGroup((status.group as Group)?.id); + const { data: group } = useGroupQuery(status.group_id || undefined, true); const { mutate: blockGroupMember } = useBlockGroupUserMutation( status.group?.id as string, status.account.id, @@ -751,7 +751,6 @@ const MenuButton: React.FC = ({ const { mutate: unpinStatus } = useUnpinStatus(status?.id); const { mutate: unblockAccount } = useUnblockAccountMutation(status.account_id); - const { groupRelationship } = useGroupRelationship(status.group_id ?? undefined); const features = useFeatures(); const instance = useInstance(); const { autoTranslate, deleteModal, knownLanguages } = useSettings(); @@ -1229,8 +1228,8 @@ const MenuButton: React.FC = ({ } if (isGroupStatus && !!status.group) { - const isGroupOwner = groupRelationship?.role === GroupRoles.OWNER; - const isGroupAdmin = groupRelationship?.role === GroupRoles.ADMIN; + const isGroupOwner = group?.relationship?.role === GroupRoles.OWNER; + const isGroupAdmin = group?.relationship?.role === GroupRoles.ADMIN; // const isStatusFromOwner = group.owner.id === account.id; const canBanUser = match && (isGroupOwner || isGroupAdmin) && !ownAccount; diff --git a/packages/pl-fe/src/features/group/components/group-action-button.tsx b/packages/pl-fe/src/features/group/components/group-action-button.tsx index a10d35c27..671b2e34c 100644 --- a/packages/pl-fe/src/features/group/components/group-action-button.tsx +++ b/packages/pl-fe/src/features/group/components/group-action-button.tsx @@ -2,16 +2,15 @@ import { GroupRoles } from 'pl-api'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useJoinGroup } from '@/api/hooks/groups/use-join-group'; -import { useLeaveGroup } from '@/api/hooks/groups/use-leave-group'; import Button from '@/components/ui/button'; -import { importEntities } from '@/entity-store/actions'; -import { Entities } from '@/entity-store/entities'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { + useJoinGroupMutation, + useLeaveGroupMutation, +} from '@/queries/groups/use-group-relationship'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; -import type { Group, GroupRelationship } from 'pl-api'; +import type { Group } from 'pl-api'; interface IGroupActionButton { group: Pick; @@ -33,12 +32,11 @@ const messages = defineMessages({ }); const GroupActionButton = ({ group }: IGroupActionButton) => { - const dispatch = useAppDispatch(); const intl = useIntl(); const { openModal } = useModalsActions(); - const joinGroup = useJoinGroup(group); - const leaveGroup = useLeaveGroup(group); + const { mutate: joinGroup, isPending: isJoiningGroup } = useJoinGroupMutation(group.id); + const { mutate: leaveGroup, isPending: isLeavingGroup } = useLeaveGroupMutation(group.id); const isRequested = group.relationship?.requested; const isNonMember = !group.relationship?.member && !isRequested; @@ -46,26 +44,21 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { const isAdmin = group.relationship?.role === GroupRoles.ADMIN; const onJoinGroup = () => - joinGroup.mutate( - {}, - { - onSuccess() { - joinGroup.invalidate(); - - toast.success( - group.locked - ? intl.formatMessage(messages.joinRequestSuccess) - : intl.formatMessage(messages.joinSuccess), - ); - }, - onError(error) { - const message = error.response?.json?.error; - if (message) { - toast.error(message); - } - }, + joinGroup(undefined, { + onSuccess: () => { + toast.success( + group.locked + ? intl.formatMessage(messages.joinRequestSuccess) + : intl.formatMessage(messages.joinSuccess), + ); }, - ); + // onError: (error) => { + // const message = error.response?.json?.error; + // if (message) { + // toast.error(message); + // } + // }, + }); const onLeaveGroup = () => { openModal('CONFIRM', { @@ -73,25 +66,15 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { message: intl.formatMessage(messages.confirmationMessage), confirm: intl.formatMessage(messages.confirmationConfirm), onConfirm: () => - leaveGroup.mutate(group.relationship?.id as string, { - onSuccess() { - leaveGroup.invalidate(); + leaveGroup(undefined, { + onSuccess: () => { toast.success(intl.formatMessage(messages.leaveSuccess)); }, }), }); }; - const onCancelRequest = () => - leaveGroup.mutate(group.relationship?.id as string, { - onSuccess() { - const entity = { - ...(group.relationship as GroupRelationship), - requested: false, - }; - dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS)); - }, - }); + const onCancelRequest = () => leaveGroup(); if (isOwner || isAdmin) { return ( @@ -103,7 +86,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { if (isNonMember) { return ( - ); } return ( - ); diff --git a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx index c283de365..43abae6ff 100644 --- a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx +++ b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx @@ -4,8 +4,6 @@ import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useAccount } from '@/api/hooks/accounts/use-account'; -import { useDemoteGroupMember } from '@/api/hooks/groups/use-demote-group-member'; -import { usePromoteGroupMember } from '@/api/hooks/groups/use-promote-group-member'; import Account from '@/components/account'; import DropdownMenu from '@/components/dropdown-menu/dropdown-menu'; import HStack from '@/components/ui/hstack'; @@ -15,7 +13,9 @@ import PlaceholderAccount from '@/features/placeholder/components/placeholder-ac import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useBlockGroupUserMutation } from '@/queries/groups/use-group-blocks'; import { + useDemoteGroupMemberMutation, useKickGroupMemberMutation, + usePromoteGroupMemberMutation, type MinifiedGroupMember, } from '@/queries/groups/use-group-members'; import { useModalsActions } from '@/stores/modals'; @@ -79,8 +79,8 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { const { mutate: blockGroupMember } = useBlockGroupUserMutation(group.id, member.account_id); const { mutate: kickGroupMember } = useKickGroupMemberMutation(group.id, member.account_id); - const promoteGroupMember = usePromoteGroupMember(group, member); - const demoteGroupMember = useDemoteGroupMember(group, member); + const { mutate: promoteGroupMember } = usePromoteGroupMemberMutation(group.id); + const { mutate: demoteGroupMember } = useDemoteGroupMemberMutation(group.id); const { account, isLoading } = useAccount(member.account_id); @@ -91,6 +91,7 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { // Member role const isMemberOwner = member.role === GroupRoles.OWNER; const isMemberAdmin = member.role === GroupRoles.ADMIN; + // const isMemberModerator = membisMemberModeratorer.role === GroupRoles.MODERATOR; const isMemberUser = member.role === GroupRoles.USER; const handleKickFromGroup = () => { @@ -131,7 +132,7 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { confirm: intl.formatMessage(messages.promoteConfirm), onConfirm: () => { promoteGroupMember( - { role: GroupRoles.ADMIN, account_ids: [member.account_id] }, + { accountId: member.account_id, role: GroupRoles.ADMIN }, { onSuccess() { toast.success(intl.formatMessage(messages.promotedToAdmin, { name: account?.acct })); @@ -144,7 +145,7 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { const handleUserAssignment = () => { demoteGroupMember( - { role: GroupRoles.USER, account_ids: [member.account_id] }, + { accountId: member.account_id, role: GroupRoles.USER }, { onSuccess() { toast.success(intl.formatMessage(messages.demotedToUser, { name: account?.acct })); diff --git a/packages/pl-fe/src/features/group/components/group-options-button.tsx b/packages/pl-fe/src/features/group/components/group-options-button.tsx index 1f67cfa39..ecf0fe421 100644 --- a/packages/pl-fe/src/features/group/components/group-options-button.tsx +++ b/packages/pl-fe/src/features/group/components/group-options-button.tsx @@ -2,9 +2,9 @@ import { GroupRoles, type Group } from 'pl-api'; import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useLeaveGroup } from '@/api/hooks/groups/use-leave-group'; import DropdownMenu, { Menu } from '@/components/dropdown-menu'; import IconButton from '@/components/ui/icon-button'; +import { useLeaveGroupMutation } from '@/queries/groups/use-group-relationship'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -28,7 +28,7 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => { const { openModal } = useModalsActions(); const intl = useIntl(); - const leaveGroup = useLeaveGroup(group); + const { mutate: leaveGroup } = useLeaveGroupMutation(group.id); const isMember = group.relationship?.role === GroupRoles.USER; const isAdmin = group.relationship?.role === GroupRoles.ADMIN; @@ -51,9 +51,8 @@ const GroupOptionsButton = ({ group }: IGroupActionButton) => { message: intl.formatMessage(messages.confirmationMessage), confirm: intl.formatMessage(messages.confirmationConfirm), onConfirm: () => - leaveGroup.mutate(group.relationship?.id as string, { - onSuccess() { - leaveGroup.invalidate(); + leaveGroup(undefined, { + onSuccess: () => { toast.success(intl.formatMessage(messages.leaveSuccess)); }, }), diff --git a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx index 82ba85339..c099eaa84 100644 --- a/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx +++ b/packages/pl-fe/src/features/groups/components/discover/group-list-item.tsx @@ -9,27 +9,18 @@ import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import GroupActionButton from '@/features/group/components/group-action-button'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { shortNumberFormat } from '@/utils/numbers'; -import type { Group } from 'pl-api'; - interface IGroupListItem { - group: Pick< - Group, - | 'id' - | 'avatar' - | 'avatar_description' - | 'display_name' - | 'emojis' - | 'locked' - | 'members_count' - | 'relationship' - >; + groupId: string; withJoinAction?: boolean; } -const GroupListItem = (props: IGroupListItem) => { - const { group, withJoinAction = true } = props; +const GroupListItem: React.FC = ({ groupId, withJoinAction = true }) => { + const { data: group } = useGroupQuery(groupId, true); + + if (!group) return null; return ( diff --git a/packages/pl-fe/src/features/ui/components/compose-button.tsx b/packages/pl-fe/src/features/ui/components/compose-button.tsx index 951a3bb55..3748e352a 100644 --- a/packages/pl-fe/src/features/ui/components/compose-button.tsx +++ b/packages/pl-fe/src/features/ui/components/compose-button.tsx @@ -3,11 +3,11 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { groupComposeModal } from '@/actions/compose'; -import { useGroup } from '@/api/hooks/groups/use-group'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; import Icon from '@/components/ui/icon'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { useModalsActions } from '@/stores/modals'; import { layouts } from '../router'; @@ -19,7 +19,7 @@ interface IComposeButton { const ComposeButton: React.FC = ({ shrink }) => { const match = useMatch({ from: layouts.group.id, shouldThrow: false }); - const { group } = useGroup(match?.params.groupId ?? ''); + const { data: group } = useGroupQuery(match?.params.groupId); const isGroupMember = !!group?.relationship?.member; if (match && isGroupMember) { @@ -49,7 +49,7 @@ const HomeComposeButton: React.FC = ({ shrink }) => { const GroupComposeButton: React.FC = ({ shrink }) => { const dispatch = useAppDispatch(); const match = useMatch({ from: layouts.group.id, shouldThrow: false }); - const { group } = useGroup(match?.params.groupId ?? ''); + const { data: group } = useGroupQuery(match?.params.groupId); if (!group) return null; diff --git a/packages/pl-fe/src/features/ui/components/panels/my-groups-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/my-groups-panel.tsx index 7d7c59df0..ead19bba7 100644 --- a/packages/pl-fe/src/features/ui/components/panels/my-groups-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/my-groups-panel.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useGroups } from '@/api/hooks/groups/use-groups'; import Widget from '@/components/ui/widget'; import GroupListItem from '@/features/groups/components/discover/group-list-item'; import PlaceholderGroupSearch from '@/features/placeholder/components/placeholder-group-search'; +import { useGroupsQuery } from '@/queries/groups/use-groups'; const MyGroupsPanel = () => { - const { groups, isFetching, isFetched, isError } = useGroups(); - const isEmpty = (isFetched && groups.length === 0) ?? isError; + const { data: groupIds = [], isFetching, isError } = useGroupsQuery(); + const isEmpty = (!isFetching && groupIds.length === 0) ?? isError; if (isEmpty) { return null; @@ -20,9 +20,11 @@ const MyGroupsPanel = () => { ? new Array(3) .fill(0) .map((_, idx) => ) - : groups + : groupIds .slice(0, 3) - .map((group) => )} + .map((groupId) => ( + + ))} ); }; diff --git a/packages/pl-fe/src/layouts/group-layout.tsx b/packages/pl-fe/src/layouts/group-layout.tsx index 392cfa679..7484128db 100644 --- a/packages/pl-fe/src/layouts/group-layout.tsx +++ b/packages/pl-fe/src/layouts/group-layout.tsx @@ -2,8 +2,6 @@ import { Outlet, useLocation } from '@tanstack/react-router'; import React, { useMemo } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useGroup } from '@/api/hooks/groups/use-group'; -import { useGroupMembershipRequests } from '@/api/hooks/groups/use-group-membership-requests'; import Column from '@/components/ui/column'; import Icon from '@/components/ui/icon'; import Layout from '@/components/ui/layout'; @@ -15,6 +13,8 @@ import LinkFooter from '@/features/ui/components/link-footer'; import { layouts } from '@/features/ui/router'; import { GroupMediaPanel, SignUpPanel } from '@/features/ui/util/async-components'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useGroupQuery } from '@/queries/groups/use-group'; +import { useGroupMembershipRequestsQuery } from '@/queries/groups/use-group-members'; const messages = defineMessages({ all: { id: 'group.tabs.all', defaultMessage: 'All' }, @@ -48,8 +48,8 @@ const GroupLayout = () => { const location = useLocation(); const { account: me } = useOwnAccount(); - const { group } = useGroup(groupId); - const { accounts: pending } = useGroupMembershipRequests(groupId); + const { data: group } = useGroupQuery(groupId, true); + const { data: membershipRequests = [] } = useGroupMembershipRequestsQuery(groupId); const isMember = !!group?.relationship?.member; const isPrivate = group?.locked; @@ -76,12 +76,12 @@ const GroupLayout = () => { to: '/groups/$groupId/members', params: { groupId }, name: '/groups/$groupId/members', - count: pending.length, + count: membershipRequests.length, }, ); return items; - }, [pending.length, groupId]); + }, [membershipRequests.length, groupId]); const renderChildren = () => { if (!isMember && isPrivate) { diff --git a/packages/pl-fe/src/pages/groups/edit-group.tsx b/packages/pl-fe/src/pages/groups/edit-group.tsx index 95d081a65..fc5043371 100644 --- a/packages/pl-fe/src/pages/groups/edit-group.tsx +++ b/packages/pl-fe/src/pages/groups/edit-group.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useGroup } from '@/api/hooks/groups/use-group'; import { useUpdateGroup } from '@/api/hooks/groups/use-update-group'; import Button from '@/components/ui/button'; import Column from '@/components/ui/column'; @@ -19,9 +18,9 @@ import { useImageField } from '@/hooks/forms/use-image-field'; import { useTextField } from '@/hooks/forms/use-text-field'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; +import { useGroupQuery } from '@/queries/groups/use-group'; import toast from '@/toast'; import { unescapeHTML } from '@/utils/html'; - const messages = defineMessages({ heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' }, groupNamePlaceholder: { @@ -41,7 +40,7 @@ const EditGroup: React.FC = () => { const intl = useIntl(); const instance = useInstance(); - const { group, isLoading } = useGroup(groupId); + const { data: group, isLoading } = useGroupQuery(groupId); const { updateGroup } = useUpdateGroup(groupId); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx index e570ac27b..d3dd35941 100644 --- a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx +++ b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { useAccount } from '@/api/hooks/accounts/use-account'; -import { useGroup } from '@/api/hooks/groups/use-group'; import Account from '@/components/account'; import ScrollableList from '@/components/scrollable-list'; import Button from '@/components/ui/button'; @@ -11,6 +10,7 @@ import HStack from '@/components/ui/hstack'; import Spinner from '@/components/ui/spinner'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { groupBlocksRoute } from '@/features/ui/router'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { useGroupBlocks, useUnblockGroupUserMutation } from '@/queries/groups/use-group-blocks'; import toast from '@/toast'; @@ -64,7 +64,7 @@ const GroupBlockedMembers: React.FC = () => { const intl = useIntl(); - const { group } = useGroup(groupId); + const { data: group } = useGroupQuery(groupId, true); const { data: accountIds } = useGroupBlocks(groupId); if (!group || !group.relationship || !accountIds) { diff --git a/packages/pl-fe/src/pages/groups/group-gallery.tsx b/packages/pl-fe/src/pages/groups/group-gallery.tsx index f248c11f9..fee99516f 100644 --- a/packages/pl-fe/src/pages/groups/group-gallery.tsx +++ b/packages/pl-fe/src/pages/groups/group-gallery.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useGroup } from '@/api/hooks/groups/use-group'; import LoadMore from '@/components/load-more'; import MissingIndicator from '@/components/missing-indicator'; import Column from '@/components/ui/column'; @@ -9,6 +8,7 @@ import Spinner from '@/components/ui/spinner'; import { groupGalleryRoute } from '@/features/ui/router'; import { type AccountGalleryAttachment, useGroupGallery } from '@/hooks/use-account-gallery'; import { MediaItem } from '@/pages/accounts/account-gallery'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { useModalsActions } from '@/stores/modals'; const GroupGallery: React.FC = () => { @@ -16,7 +16,7 @@ const GroupGallery: React.FC = () => { const { openModal } = useModalsActions(); - const { group, isLoading: groupIsLoading } = useGroup(groupId); + const { data: group, isLoading: groupIsLoading } = useGroupQuery(groupId, true); const { data: attachments, diff --git a/packages/pl-fe/src/pages/groups/group-members.tsx b/packages/pl-fe/src/pages/groups/group-members.tsx index 6c85a1eb7..0b2129a29 100644 --- a/packages/pl-fe/src/pages/groups/group-members.tsx +++ b/packages/pl-fe/src/pages/groups/group-members.tsx @@ -2,19 +2,21 @@ import clsx from 'clsx'; import { GroupRoles } from 'pl-api'; import React, { useMemo } from 'react'; -import { useGroup } from '@/api/hooks/groups/use-group'; -import { useGroupMembershipRequests } from '@/api/hooks/groups/use-group-membership-requests'; import { PendingItemsRow } from '@/components/pending-items-row'; import ScrollableList from '@/components/scrollable-list'; import GroupMemberListItem from '@/features/group/components/group-member-list-item'; import PlaceholderAccount from '@/features/placeholder/components/placeholder-account'; import { groupMembersRoute } from '@/features/ui/router'; -import { useGroupMembers } from '@/queries/groups/use-group-members'; +import { useGroupQuery } from '@/queries/groups/use-group'; +import { + useGroupMembers, + useGroupMembershipRequestsQuery, +} from '@/queries/groups/use-group-members'; const GroupMembers: React.FC = () => { const { groupId } = groupMembersRoute.useParams(); - const { group, isFetching: isFetchingGroup } = useGroup(groupId); + const { data: group, isFetching: isFetchingGroup } = useGroupQuery(groupId, true); const { data: owners, isFetching: isFetchingOwners } = useGroupMembers(groupId, GroupRoles.OWNER); const { data: admins, isFetching: isFetchingAdmins } = useGroupMembers(groupId, GroupRoles.ADMIN); const { @@ -23,8 +25,8 @@ const GroupMembers: React.FC = () => { fetchNextPage, hasNextPage, } = useGroupMembers(groupId, GroupRoles.USER); - const { isFetching: isFetchingPending, count: pendingCount } = - useGroupMembershipRequests(groupId); + const { isFetching: isFetchingPending, data: membershipRequests = [] } = + useGroupMembershipRequestsQuery(groupId); const isLoading = isFetchingGroup || isFetchingOwners || isFetchingAdmins || isFetchingUsers || isFetchingPending; @@ -47,7 +49,7 @@ const GroupMembers: React.FC = () => { className='⁂-status-list' itemClassName='py-3 last:pb-0' prepend={ - pendingCount > 0 && ( + membershipRequests.length > 0 && (
{ >
) diff --git a/packages/pl-fe/src/pages/groups/group-membership-requests.tsx b/packages/pl-fe/src/pages/groups/group-membership-requests.tsx index 286439ee5..251abe881 100644 --- a/packages/pl-fe/src/pages/groups/group-membership-requests.tsx +++ b/packages/pl-fe/src/pages/groups/group-membership-requests.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useGroup } from '@/api/hooks/groups/use-group'; -import { useGroupMembershipRequests } from '@/api/hooks/groups/use-group-membership-requests'; +import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import { AuthorizeRejectButtons } from '@/components/authorize-reject-buttons'; import ScrollableList from '@/components/scrollable-list'; @@ -11,6 +10,12 @@ import HStack from '@/components/ui/hstack'; import Spinner from '@/components/ui/spinner'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { groupMembershipRequestsRoute } from '@/features/ui/router'; +import { useGroupQuery } from '@/queries/groups/use-group'; +import { + useAcceptGroupMembershipRequestMutation, + useGroupMembershipRequestsQuery, + useRejectGroupMembershipRequestMutation, +} from '@/queries/groups/use-group-members'; import toast from '@/toast'; import type { PlfeResponse } from '@/api'; @@ -26,12 +31,14 @@ const messages = defineMessages({ }); interface IMembershipRequest { - account: AccountEntity; + accountId: string; onAuthorize(account: AccountEntity): Promise; onReject(account: AccountEntity): Promise; } -const MembershipRequest: React.FC = ({ account, onAuthorize, onReject }) => { +const MembershipRequest: React.FC = ({ accountId, onAuthorize, onReject }) => { + const { account } = useAccount(); + if (!account) return null; const handleAuthorize = () => onAuthorize(account); @@ -57,11 +64,13 @@ const GroupMembershipRequests: React.FC = () => { const intl = useIntl(); - const { group } = useGroup(groupId); + const { data: group } = useGroupQuery(groupId, true); - const { accounts, authorize, reject, refetch, isLoading } = useGroupMembershipRequests(groupId); + const { data: accountIds = [], isFetching } = useGroupMembershipRequestsQuery(groupId); + const { mutate: acceptGroupMembershipRequest } = useAcceptGroupMembershipRequestMutation(groupId); + const { mutate: rejectGroupMembershipRequest } = useRejectGroupMembershipRequestMutation(groupId); - if (!group || !group.relationship || isLoading) { + if (!group || !group.relationship || isFetching) { return ( @@ -76,39 +85,41 @@ const GroupMembershipRequests: React.FC = () => { return ; } - const handleAuthorize = async (account: AccountEntity) => { - try { - await authorize(account.id); - } catch (error) { - const { response } = error as { response: PlfeResponse }; + const handleAuthorize = (account: AccountEntity) => + new Promise((resolve, reject) => { + acceptGroupMembershipRequest(account.id, { + onSuccess: () => resolve(), + onError: (error) => { + const { response } = error as unknown as { response: PlfeResponse }; - refetch(); + let message = intl.formatMessage(messages.authorizeFail, { name: account.username }); + if (response?.status === 409) { + message = response.json.error; + } + toast.error(message); - let message = intl.formatMessage(messages.authorizeFail, { name: account.username }); - if (response?.status === 409) { - message = response.json.error; - } - toast.error(message); - } - }; + reject(); + }, + }); + }); - const handleReject = async (account: AccountEntity) => { - try { - await reject(account.id); - } catch (error) { - const { response } = error as { response: PlfeResponse }; + const handleReject = (account: AccountEntity) => + new Promise((resolve, reject) => { + rejectGroupMembershipRequest(account.id, { + onSuccess: () => resolve(), + onError: (error) => { + const { response } = error as unknown as { response: PlfeResponse }; - refetch(); + let message = intl.formatMessage(messages.rejectFail, { name: account.username }); + if (response?.status === 409) { + message = response.json.error; + } + toast.error(message); - let message = intl.formatMessage(messages.rejectFail, { name: account.username }); - if (response?.status === 409) { - message = response.json.error; - } - toast.error(message); - - return Promise.reject(); - } - }; + reject(); + }, + }); + }); return ( @@ -121,10 +132,10 @@ const GroupMembershipRequests: React.FC = () => { /> } > - {accounts.map((account) => ( + {accountIds.map((account) => ( diff --git a/packages/pl-fe/src/pages/groups/groups.tsx b/packages/pl-fe/src/pages/groups/groups.tsx index e072f314b..5dbb84032 100644 --- a/packages/pl-fe/src/pages/groups/groups.tsx +++ b/packages/pl-fe/src/pages/groups/groups.tsx @@ -2,25 +2,19 @@ import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useGroups } from '@/api/hooks/groups/use-groups'; import GroupCard from '@/components/group-card'; import ScrollableList from '@/components/scrollable-list'; import Button from '@/components/ui/button'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import PlaceholderGroupCard from '@/features/placeholder/components/placeholder-group-card'; +import { useGroupsQuery } from '@/queries/groups/use-groups'; import { useModalsActions } from '@/stores/modals'; const Groups: React.FC = () => { const { openModal } = useModalsActions(); - const { groups, isLoading, hasNextPage, fetchNextPage } = useGroups(); - - const handleLoadMore = () => { - if (hasNextPage) { - fetchNextPage(); - } - }; + const { data: groupIds = [], isFetching, isLoading } = useGroupsQuery(); const createGroup = () => { openModal('CREATE_GROUP'); @@ -49,7 +43,7 @@ const Groups: React.FC = () => { return ( - {!(!isLoading && groups.length === 0) && ( + {!(!isFetching && groupIds.length === 0) && ( diff --git a/packages/pl-fe/src/pages/groups/manage-group.tsx b/packages/pl-fe/src/pages/groups/manage-group.tsx index 76611b632..c1ea5ffe5 100644 --- a/packages/pl-fe/src/pages/groups/manage-group.tsx +++ b/packages/pl-fe/src/pages/groups/manage-group.tsx @@ -3,7 +3,6 @@ import { GroupRoles } from 'pl-api'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useDeleteGroup } from '@/api/hooks/groups/use-delete-group'; import List, { ListItem } from '@/components/list'; import { CardBody, CardHeader, CardTitle } from '@/components/ui/card'; import Column from '@/components/ui/column'; @@ -12,7 +11,7 @@ import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { manageGroupRoute } from '@/features/ui/router'; -import { useGroupQuery } from '@/queries/groups/use-group'; +import { useDeleteGroupMutation, useGroupQuery } from '@/queries/groups/use-group'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -43,7 +42,7 @@ const ManageGroup: React.FC = () => { const { data: group } = useGroupQuery(groupId, true); - const deleteGroup = useDeleteGroup(); + const { mutate: deleteGroup } = useDeleteGroupMutation(groupId); const isOwner = group?.relationship?.role === GroupRoles.OWNER; @@ -68,7 +67,7 @@ const ManageGroup: React.FC = () => { message: intl.formatMessage(messages.deleteMessage), confirm: intl.formatMessage(messages.deleteConfirm), onConfirm: () => { - deleteGroup.mutate(groupId, { + deleteGroup(undefined, { onSuccess() { toast.success(intl.formatMessage(messages.deleteSuccess)); navigate({ to: '/groups' }); diff --git a/packages/pl-fe/src/queries/groups/use-group.ts b/packages/pl-fe/src/queries/groups/use-group.ts index 00931c5d3..e05330e88 100644 --- a/packages/pl-fe/src/queries/groups/use-group.ts +++ b/packages/pl-fe/src/queries/groups/use-group.ts @@ -1,10 +1,12 @@ -import { useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useClient } from '@/hooks/use-client'; import { useGroupRelationshipQuery } from './use-group-relationship'; +import type { CreateGroupParams, UpdateGroupParams } from 'pl-api'; + const useGroupQuery = (groupId?: string, withRelationship = true) => { const client = useClient(); @@ -30,4 +32,46 @@ const useGroupQuery = (groupId?: string, withRelationship = true) => { ); }; -export { useGroupQuery }; +const useCreateGroupMutation = () => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'create'], + mutationFn: (params: CreateGroupParams) => client.experimental.groups.createGroup(params), + onSuccess: (data) => { + queryClient.setQueryData(['groups', data.id], data); + queryClient.invalidateQueries({ queryKey: ['groupLists', 'myGroups'] }); + }, + }); +}; + +const useUpdateGroupMutation = (groupId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'update'], + mutationFn: (params: UpdateGroupParams) => + client.experimental.groups.updateGroup(groupId, params), + onSuccess: (data) => { + queryClient.setQueryData(['groups', data.id], data); + }, + }); +}; + +const useDeleteGroupMutation = (groupId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['groups', 'delete'], + mutationFn: () => client.experimental.groups.deleteGroup(groupId), + onSuccess: () => { + queryClient.removeQueries({ queryKey: ['groups', groupId] }); + queryClient.invalidateQueries({ queryKey: ['groupLists', 'myGroups'] }); + }, + }); +}; + +export { useGroupQuery, useCreateGroupMutation, useUpdateGroupMutation, useDeleteGroupMutation }; From 636e48821917feb5f34b08a360ada4798a761f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 20:47:14 +0100 Subject: [PATCH 010/264] nicolium: group migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/importer.ts | 6 ++- packages/pl-fe/src/components/status.tsx | 1 + .../pl-fe/src/components/thumb-navigation.tsx | 7 +++- packages/pl-fe/src/entity-store/actions.ts | 35 ++-------------- packages/pl-fe/src/entity-store/reducer.ts | 41 +------------------ packages/pl-fe/src/entity-store/types.ts | 6 --- packages/pl-fe/src/entity-store/utils.ts | 8 ---- .../components/reply-group-indicator.tsx | 4 +- .../status/components/detailed-status.tsx | 7 +--- packages/pl-fe/src/selectors/index.ts | 20 +-------- 10 files changed, 23 insertions(+), 112 deletions(-) diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index e0a8251df..f1e922f0d 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -10,6 +10,7 @@ import type { Poll as BasePoll, Relationship as BaseRelationship, Status as BaseStatus, + GroupRelationship, } from 'pl-api'; const STATUS_IMPORT = 'STATUS_IMPORT' as const; @@ -112,7 +113,10 @@ const importEntities = for (const group of Object.values(groups)) { queryClient.setQueryData(['groups', group.id], group); if (group.relationship) { - queryClient.setQueryData(['groupRelationships', group.id], group.relationship); + queryClient.setQueryData( + ['groupRelationships', group.id], + group.relationship, + ); } } if (!isEmpty(polls)) { diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 05fa45847..ce1a34b1a 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -14,6 +14,7 @@ import StatusTypeIcon from '@/features/status/components/status-type-icon'; import { Hotkeys } from '@/features/ui/components/hotkeys'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { useFollowedTags } from '@/queries/hashtags/use-followed-tags'; import { useFavouriteStatus, diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index ed0d45591..459531122 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -1,3 +1,4 @@ +import { useQueryClient } from '@tanstack/react-query'; import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -6,7 +7,6 @@ import { groupComposeModal } from '@/actions/compose'; import ThumbNavigationLink from '@/components/thumb-navigation-link'; import Icon from '@/components/ui/icon'; import { useStatContext } from '@/contexts/stat-context'; -import { Entities } from '@/entity-store/entities'; import { layouts } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; @@ -16,6 +16,8 @@ import { useModalsActions } from '@/stores/modals'; import { useIsSidebarOpen, useUiStoreActions } from '@/stores/ui'; import { isStandalone } from '@/utils/state'; +import type { Group } from 'pl-api'; + const messages = defineMessages({ home: { id: 'column.home', defaultMessage: 'Home' }, search: { id: 'column.search', defaultMessage: 'Search' }, @@ -31,6 +33,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => { const dispatch = useAppDispatch(); const { account } = useOwnAccount(); const features = useFeatures(); + const queryClient = useQueryClient(); const match = useMatch({ from: layouts.group.id, shouldThrow: false }); @@ -45,7 +48,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => { const handleOpenComposeModal = () => { if (match?.params.groupId) { dispatch((_, getState) => { - const group = getState().entities[Entities.GROUPS]?.store[match.params.groupId]; + const group = queryClient.getQueryData(['groups', match.params.groupId]); if (group) dispatch(groupComposeModal(group)); }); } else { diff --git a/packages/pl-fe/src/entity-store/actions.ts b/packages/pl-fe/src/entity-store/actions.ts index 5b4f71397..6d4157b98 100644 --- a/packages/pl-fe/src/entity-store/actions.ts +++ b/packages/pl-fe/src/entity-store/actions.ts @@ -1,37 +1,14 @@ import type { Entities } from './entities'; -import type { EntitiesTransaction, Entity, ImportPosition } from './types'; +import type { EntitiesTransaction, Entity } from './types'; const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const; -const ENTITIES_DELETE = 'ENTITIES_DELETE' as const; const ENTITIES_TRANSACTION = 'ENTITIES_TRANSACTION' as const; /** Action to import entities into the cache. */ -const importEntities = ( - entities: Entity[], - entityType: Entities, - listKey?: string, - pos?: ImportPosition, -) => ({ +const importEntities = (entities: Entity[], entityType: Entities) => ({ type: ENTITIES_IMPORT, entityType, entities, - listKey, - pos, -}); - -interface DeleteEntitiesOpts { - preserveLists?: boolean; -} - -const deleteEntities = ( - ids: Iterable, - entityType: string, - opts: DeleteEntitiesOpts = {}, -) => ({ - type: ENTITIES_DELETE, - ids, - entityType, - opts, }); const entitiesTransaction = (transaction: EntitiesTransaction) => ({ @@ -40,18 +17,12 @@ const entitiesTransaction = (transaction: EntitiesTransaction) => ({ }); /** Any action pertaining to entities. */ -type EntityAction = - | ReturnType - | ReturnType - | ReturnType; +type EntityAction = ReturnType | ReturnType; export { - type DeleteEntitiesOpts, type EntityAction, ENTITIES_IMPORT, - ENTITIES_DELETE, ENTITIES_TRANSACTION, importEntities, - deleteEntities, entitiesTransaction, }; diff --git a/packages/pl-fe/src/entity-store/reducer.ts b/packages/pl-fe/src/entity-store/reducer.ts index 599af63d7..dd52dbd6c 100644 --- a/packages/pl-fe/src/entity-store/reducer.ts +++ b/packages/pl-fe/src/entity-store/reducer.ts @@ -1,12 +1,6 @@ import { create, type Immutable, type Draft } from 'mutative'; -import { - ENTITIES_IMPORT, - ENTITIES_DELETE, - ENTITIES_TRANSACTION, - type EntityAction, - type DeleteEntitiesOpts, -} from './actions'; +import { ENTITIES_IMPORT, ENTITIES_TRANSACTION, type EntityAction } from './actions'; import { Entities } from './entities'; import { createCache, createList, updateStore, updateList } from './utils'; @@ -55,33 +49,6 @@ const importEntities = ( draft[entityType] = cache; }; -const deleteEntities = ( - draft: Draft, - entityType: string, - ids: Iterable, - opts: DeleteEntitiesOpts, -) => { - const cache = draft[entityType] ?? createCache(); - - for (const id of ids) { - delete cache.store[id]; - - if (!opts?.preserveLists) { - for (const list of Object.values(cache.lists)) { - if (list) { - list.ids.delete(id); - - if (typeof list.state.totalCount === 'number') { - list.state.totalCount--; - } - } - } - } - } - - draft[entityType] = cache; -}; - const doTransaction = (draft: Draft, transaction: EntitiesTransaction) => { for (const [entityType, changes] of Object.entries(transaction)) { const cache = draft[entityType] ?? createCache(); @@ -101,14 +68,10 @@ const reducer = (state: Readonly = {}, action: EntityAction): State => { return create( state, (draft) => { - importEntities(draft, action.entityType, action.entities, action.listKey, action.pos); + importEntities(draft, action.entityType, action.entities); }, { enableAutoFreeze: true }, ); - case ENTITIES_DELETE: - return create(state, (draft) => { - deleteEntities(draft, action.entityType, action.ids, action.opts); - }); case ENTITIES_TRANSACTION: return create(state, (draft) => { doTransaction(draft, action.transaction); diff --git a/packages/pl-fe/src/entity-store/types.ts b/packages/pl-fe/src/entity-store/types.ts index 925d61c5e..7bf6c6654 100644 --- a/packages/pl-fe/src/entity-store/types.ts +++ b/packages/pl-fe/src/entity-store/types.ts @@ -25,18 +25,12 @@ interface EntityListState { next: (() => Promise>) | null; /** Previous URL for pagination, if any. */ prev: (() => Promise>) | null; - /** Total number of items according to the API. */ - totalCount: number | undefined; /** Error returned from the API, if any. */ error: unknown; /** Whether data has already been fetched */ fetched: boolean; /** Whether data for this list is currently being fetched. */ fetching: boolean; - /** Date of the last API fetch for this list. */ - lastFetchedAt: Date | undefined; - /** Whether the entities should be refetched on the next component mount. */ - invalid: boolean; } /** Cache data pertaining to a paritcular entity type.. */ diff --git a/packages/pl-fe/src/entity-store/utils.ts b/packages/pl-fe/src/entity-store/utils.ts index 73d2f087b..f536d8a99 100644 --- a/packages/pl-fe/src/entity-store/utils.ts +++ b/packages/pl-fe/src/entity-store/utils.ts @@ -27,11 +27,6 @@ const updateList = ( const oldIds = Array.from(list.ids); const ids = new Set(pos === 'start' ? [...newIds, ...oldIds] : [...oldIds, ...newIds]); - if (typeof list.state.totalCount === 'number') { - const sizeDiff = ids.size - list.ids.size; - list.state.totalCount += sizeDiff; - } - return { ...list, ids, @@ -54,12 +49,9 @@ const createList = (): EntityList => ({ const createListState = (): EntityListState => ({ next: null, prev: null, - totalCount: 0, error: null, fetched: false, fetching: false, - lastFetchedAt: undefined, - invalid: false, }); export { updateStore, updateList, createCache, createList }; diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index 7c0d1dd26..fec71c11a 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -5,6 +5,7 @@ import Link from '@/components/link'; import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useGroupQuery } from '@/queries/groups/use-group'; import { makeGetStatus } from '@/selectors'; interface IReplyGroupIndicator { @@ -19,7 +20,8 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { const status = useAppSelector((state) => getStatus(state, { id: state.compose[composeId]?.inReplyToId! }), ); - const group = status?.group; + + const { data: group } = useGroupQuery(status?.group_id ?? undefined); if (!group) { return null; diff --git a/packages/pl-fe/src/features/status/components/detailed-status.tsx b/packages/pl-fe/src/features/status/components/detailed-status.tsx index 66c74ded6..13b73e517 100644 --- a/packages/pl-fe/src/features/status/components/detailed-status.tsx +++ b/packages/pl-fe/src/features/status/components/detailed-status.tsx @@ -69,15 +69,12 @@ const DetailedStatus: React.FC = ({ group: ( - + diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index 2feaa5337..b9cb5695a 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -12,7 +12,7 @@ import type { minifyAdminReport } from '@/queries/utils/minify-list'; import type { MinifiedStatus } from '@/reducers/statuses'; import type { MRFSimple } from '@/schemas/pleroma'; import type { RootState } from '@/store'; -import type { Account, Filter, FilterResult, Group, NotificationGroup } from 'pl-api'; +import type { Account, Filter, FilterResult, NotificationGroup } from 'pl-api'; const selectAccount = (state: RootState, accountId: string) => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined; @@ -120,11 +120,6 @@ const makeGetStatus = () => state.statuses[state.statuses[id]?.reblog_id ?? ''] || null, (state: RootState, { id }: APIStatus) => state.statuses[state.statuses[id]?.quote_id ?? ''] || null, - (state: RootState, { id }: APIStatus) => { - const group = state.statuses[id]?.group_id; - if (group) return state.entities[Entities.GROUPS]?.store[group] as Group; - return undefined; - }, (_state: RootState, { username }: APIStatus) => username, (state: RootState) => state.filters, (_state: RootState, { contextType }: FilterContext) => contextType, @@ -132,17 +127,7 @@ const makeGetStatus = () => (state: RootState) => state.auth.client.features, ], - ( - statusBase, - statusReblog, - statusQuote, - statusGroup, - username, - filters, - contextType, - me, - features, - ) => { + (statusBase, statusReblog, statusQuote, username, filters, contextType, me, features) => { if (!statusBase) return null; const { account } = statusBase; const accountUsername = account.acct; @@ -165,7 +150,6 @@ const makeGetStatus = () => ...statusBase, reblog: statusReblog || null, quote: statusQuote || null, - group: statusGroup ?? null, filtered, }; }, From 3532282f1c8d993851174ac32601de1cfdf8e4a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 22 Feb 2026 20:52:48 +0100 Subject: [PATCH 011/264] nicolium: remove unused code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/entity-store/reducer.ts | 36 ++---------------- packages/pl-fe/src/entity-store/types.ts | 40 +------------------- packages/pl-fe/src/entity-store/utils.ts | 43 +--------------------- 3 files changed, 7 insertions(+), 112 deletions(-) diff --git a/packages/pl-fe/src/entity-store/reducer.ts b/packages/pl-fe/src/entity-store/reducer.ts index dd52dbd6c..b24dad8ef 100644 --- a/packages/pl-fe/src/entity-store/reducer.ts +++ b/packages/pl-fe/src/entity-store/reducer.ts @@ -2,15 +2,9 @@ import { create, type Immutable, type Draft } from 'mutative'; import { ENTITIES_IMPORT, ENTITIES_TRANSACTION, type EntityAction } from './actions'; import { Entities } from './entities'; -import { createCache, createList, updateStore, updateList } from './utils'; +import { createCache, updateStore } from './utils'; -import type { - EntitiesTransaction, - Entity, - EntityCache, - EntityListState, - ImportPosition, -} from './types'; +import type { EntitiesTransaction, Entity, EntityCache } from './types'; /** Entity reducer state. */ type State = Immutable<{ @@ -18,34 +12,10 @@ type State = Immutable<{ }>; /** Import entities into the cache. */ -const importEntities = ( - draft: Draft, - entityType: Entities, - entities: Entity[], - listKey?: string, - pos?: ImportPosition, - newState?: EntityListState, - overwrite = false, -) => { +const importEntities = (draft: Draft, entityType: Entities, entities: Entity[]) => { const cache = draft[entityType] ?? createCache(); cache.store = updateStore(cache.store, entities); - if (typeof listKey === 'string') { - let list = cache.lists[listKey] ?? createList(); - - if (overwrite) { - list.ids = new Set(); - } - - list = updateList(list, entities, pos); - - if (newState) { - list.state = newState; - } - - cache.lists[listKey] = list; - } - draft[entityType] = cache; }; diff --git a/packages/pl-fe/src/entity-store/types.ts b/packages/pl-fe/src/entity-store/types.ts index 7bf6c6654..cbf50cd38 100644 --- a/packages/pl-fe/src/entity-store/types.ts +++ b/packages/pl-fe/src/entity-store/types.ts @@ -1,5 +1,3 @@ -import type { PaginatedResponse } from 'pl-api'; - /** A Mastodon API entity. */ interface Entity { /** Unique ID for the entity (usually the primary key in the database). */ @@ -11,36 +9,10 @@ interface EntityStore { [id: string]: TEntity | undefined; } -/** List of entity IDs and fetch state. */ -interface EntityList { - /** Set of entity IDs in this list. */ - ids: Set; - /** Server state for this entity list. */ - state: EntityListState; -} - -/** Fetch state for an entity list. */ -interface EntityListState { - /** Next URL for pagination, if any. */ - next: (() => Promise>) | null; - /** Previous URL for pagination, if any. */ - prev: (() => Promise>) | null; - /** Error returned from the API, if any. */ - error: unknown; - /** Whether data has already been fetched */ - fetched: boolean; - /** Whether data for this list is currently being fetched. */ - fetching: boolean; -} - -/** Cache data pertaining to a paritcular entity type.. */ +/** Cache data pertaining to a paritcular entity type. */ interface EntityCache { /** Map of entities of this type. */ store: EntityStore; - /** Lists of entity IDs for a particular purpose. */ - lists: { - [listKey: string]: EntityList | undefined; - }; } /** Whether to import items at the start or end of the list. */ @@ -53,12 +25,4 @@ interface EntitiesTransaction { }; } -export type { - Entity, - EntityStore, - EntityList, - EntityListState, - EntityCache, - ImportPosition, - EntitiesTransaction, -}; +export type { Entity, EntityStore, EntityCache, ImportPosition, EntitiesTransaction }; diff --git a/packages/pl-fe/src/entity-store/utils.ts b/packages/pl-fe/src/entity-store/utils.ts index f536d8a99..d919ad280 100644 --- a/packages/pl-fe/src/entity-store/utils.ts +++ b/packages/pl-fe/src/entity-store/utils.ts @@ -1,11 +1,4 @@ -import type { - Entity, - EntityStore, - EntityList, - EntityCache, - EntityListState, - ImportPosition, -} from './types'; +import type { Entity, EntityStore, EntityCache } from './types'; /** Insert the entities into the store. */ const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => @@ -17,41 +10,9 @@ const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => { ...store }, ); -/** Update the list with new entity IDs. */ -const updateList = ( - list: EntityList, - entities: Entity[], - pos: ImportPosition = 'end', -): EntityList => { - const newIds = entities.map((entity) => entity.id); - const oldIds = Array.from(list.ids); - const ids = new Set(pos === 'start' ? [...newIds, ...oldIds] : [...oldIds, ...newIds]); - - return { - ...list, - ids, - }; -}; - /** Create an empty entity cache. */ const createCache = (): EntityCache => ({ store: {}, - lists: {}, }); -/** Create an empty entity list. */ -const createList = (): EntityList => ({ - ids: new Set(), - state: createListState(), -}); - -/** Create an empty entity list state. */ -const createListState = (): EntityListState => ({ - next: null, - prev: null, - error: null, - fetched: false, - fetching: false, -}); - -export { updateStore, updateList, createCache, createList }; +export { updateStore, createCache }; From d5d453e645eb59e81b034388757f01b9cdc5b2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 10:39:26 +0100 Subject: [PATCH 012/264] pl-api: allow importing parts of the client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/client-base.ts | 131 + packages/pl-api/lib/client.ts | 7336 +---------------- packages/pl-api/lib/client/accounts.ts | 453 + packages/pl-api/lib/client/admin.ts | 1556 ++++ packages/pl-api/lib/client/announcements.ts | 64 + packages/pl-api/lib/client/antennas.ts | 336 + packages/pl-api/lib/client/apps.ts | 33 + packages/pl-api/lib/client/async-refreshes.ts | 20 + packages/pl-api/lib/client/chats.ts | 117 + packages/pl-api/lib/client/circles.ts | 101 + packages/pl-api/lib/client/drive.ts | 132 + packages/pl-api/lib/client/emails.ts | 16 + packages/pl-api/lib/client/events.ts | 141 + packages/pl-api/lib/client/experimental.ts | 296 + packages/pl-api/lib/client/filtering.ts | 373 + .../lib/client/grouped-notifications.ts | 242 + packages/pl-api/lib/client/instance.ts | 210 + .../pl-api/lib/client/interaction-requests.ts | 57 + packages/pl-api/lib/client/lists.ts | 131 + packages/pl-api/lib/client/media.ts | 68 + packages/pl-api/lib/client/my-account.ts | 371 + packages/pl-api/lib/client/notifications.ts | 255 + packages/pl-api/lib/client/oauth.ts | 145 + packages/pl-api/lib/client/oembed.ts | 31 + packages/pl-api/lib/client/polls.ts | 34 + .../pl-api/lib/client/push-notifications.ts | 67 + .../lib/client/rss-feed-subscriptions.ts | 45 + .../pl-api/lib/client/scheduled-statuses.ts | 57 + packages/pl-api/lib/client/search.ts | 51 + packages/pl-api/lib/client/settings.ts | 823 ++ packages/pl-api/lib/client/shoutbox.ts | 69 + packages/pl-api/lib/client/statuses.ts | 600 ++ packages/pl-api/lib/client/stories.ts | 147 + packages/pl-api/lib/client/streaming.ts | 75 + packages/pl-api/lib/client/subscriptions.ts | 129 + packages/pl-api/lib/client/timelines.ts | 150 + packages/pl-api/lib/client/trends.ts | 55 + packages/pl-api/lib/client/utils.ts | 7 + packages/pl-api/lib/main.ts | 1 + packages/pl-api/lib/request.ts | 5 +- 40 files changed, 7714 insertions(+), 7216 deletions(-) create mode 100644 packages/pl-api/lib/client-base.ts create mode 100644 packages/pl-api/lib/client/accounts.ts create mode 100644 packages/pl-api/lib/client/admin.ts create mode 100644 packages/pl-api/lib/client/announcements.ts create mode 100644 packages/pl-api/lib/client/antennas.ts create mode 100644 packages/pl-api/lib/client/apps.ts create mode 100644 packages/pl-api/lib/client/async-refreshes.ts create mode 100644 packages/pl-api/lib/client/chats.ts create mode 100644 packages/pl-api/lib/client/circles.ts create mode 100644 packages/pl-api/lib/client/drive.ts create mode 100644 packages/pl-api/lib/client/emails.ts create mode 100644 packages/pl-api/lib/client/events.ts create mode 100644 packages/pl-api/lib/client/experimental.ts create mode 100644 packages/pl-api/lib/client/filtering.ts create mode 100644 packages/pl-api/lib/client/grouped-notifications.ts create mode 100644 packages/pl-api/lib/client/instance.ts create mode 100644 packages/pl-api/lib/client/interaction-requests.ts create mode 100644 packages/pl-api/lib/client/lists.ts create mode 100644 packages/pl-api/lib/client/media.ts create mode 100644 packages/pl-api/lib/client/my-account.ts create mode 100644 packages/pl-api/lib/client/notifications.ts create mode 100644 packages/pl-api/lib/client/oauth.ts create mode 100644 packages/pl-api/lib/client/oembed.ts create mode 100644 packages/pl-api/lib/client/polls.ts create mode 100644 packages/pl-api/lib/client/push-notifications.ts create mode 100644 packages/pl-api/lib/client/rss-feed-subscriptions.ts create mode 100644 packages/pl-api/lib/client/scheduled-statuses.ts create mode 100644 packages/pl-api/lib/client/search.ts create mode 100644 packages/pl-api/lib/client/settings.ts create mode 100644 packages/pl-api/lib/client/shoutbox.ts create mode 100644 packages/pl-api/lib/client/statuses.ts create mode 100644 packages/pl-api/lib/client/stories.ts create mode 100644 packages/pl-api/lib/client/streaming.ts create mode 100644 packages/pl-api/lib/client/subscriptions.ts create mode 100644 packages/pl-api/lib/client/timelines.ts create mode 100644 packages/pl-api/lib/client/trends.ts create mode 100644 packages/pl-api/lib/client/utils.ts diff --git a/packages/pl-api/lib/client-base.ts b/packages/pl-api/lib/client-base.ts new file mode 100644 index 000000000..983ed59d3 --- /dev/null +++ b/packages/pl-api/lib/client-base.ts @@ -0,0 +1,131 @@ +import * as v from 'valibot'; + +import { instanceSchema } from './entities/instance'; +import { filteredArray } from './entities/utils'; +import { type Features, getFeatures } from './features'; +import request, { getNextLink, getPrevLink, type RequestBody } from './request'; + +import type { Instance } from './entities/instance'; +import type { Response as PlApiResponse } from './request'; +import type { PaginatedResponse } from './responses'; + +interface PlApiClientConstructorOpts { + /** Instance object to use by default, to be populated eg. from cache */ + instance?: Instance; + /** Custom authorization token to use for requests */ + customAuthorizationToken?: string; +} + +/** + * Base Mastodon API client. + * For example usage, see {@link PlApiClient}. + * @category Clients + */ +class PlApiBaseClient { + baseURL: string; + #accessToken?: string; + #iceshrimpAccessToken?: string; + #customAuthorizationToken?: string; + #instance: Instance = v.parse(instanceSchema, {}); + public request = request.bind(this) as typeof request; + public features: Features = getFeatures(this.#instance); + /** @internal */ + socket?: { + listen: (listener: any, stream?: string) => number; + unlisten: (listener: any) => void; + subscribe: (stream: string, params?: { list?: string; tag?: string }) => void; + unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void; + close: () => void; + }; + /** @internal */ + shoutSocket?: { + message: (text: string) => void; + close: () => void; + }; + + /** + * @param baseURL Mastodon API-compatible server URL + * @param accessToken OAuth token for an authorized user + */ + constructor(baseURL: string, accessToken?: string, opts: PlApiClientConstructorOpts = {}) { + this.baseURL = baseURL; + this.#accessToken = accessToken; + this.#customAuthorizationToken = opts.customAuthorizationToken; + + if (opts.instance) { + this.setInstance(opts.instance); + } + } + + /** @internal */ + paginatedGet = async ( + input: URL | RequestInfo, + body: RequestBody, + schema: v.BaseSchema>, + isArray = true as IsArray, + ): Promise> => { + const targetSchema = isArray ? filteredArray(schema) : schema; + + const processResponse = (response: PlApiResponse) => + ({ + previous: getMore(getPrevLink(response)), + next: getMore(getNextLink(response)), + items: v.parse(targetSchema, response.json), + partial: response.status === 206, + }) as PaginatedResponse; + + const getMore = (input: string | null) => + input ? () => this.request(input).then(processResponse) : null; + + const response = await this.request(input, body); + + return processResponse(response); + }; + + /** @internal */ + setInstance = (instance: Instance) => { + this.#instance = instance; + this.features = getFeatures(this.#instance); + }; + + /** @internal */ + getIceshrimpAccessToken = async (): Promise => { + // No-op in the base client, overridden in PlApiClient + }; + + /** @internal */ + setIceshrimpAccessToken(token: string) { + this.#iceshrimpAccessToken = token; + } + + get accessToken(): string | undefined { + return this.#accessToken; + } + + set accessToken(accessToken: string | undefined) { + if (this.#accessToken === accessToken) return; + + this.socket?.close(); + this.#accessToken = accessToken; + + this.getIceshrimpAccessToken(); + } + + get iceshrimpAccessToken(): string | undefined { + return this.#iceshrimpAccessToken; + } + + get customAuthorizationToken(): string | undefined { + return this.#customAuthorizationToken; + } + + set customAuthorizationToken(token: string | undefined) { + this.#customAuthorizationToken = token; + } + + get instanceInformation() { + return this.#instance; + } +} + +export { PlApiBaseClient, type PlApiClientConstructorOpts }; diff --git a/packages/pl-api/lib/client.ts b/packages/pl-api/lib/client.ts index d8d0f1a20..744ed79ff 100644 --- a/packages/pl-api/lib/client.ts +++ b/packages/pl-api/lib/client.ts @@ -1,368 +1,108 @@ -import { WebSocket } from 'isows'; -import omit from 'lodash.omit'; -import pick from 'lodash.pick'; -import * as v from 'valibot'; +import { PlApiBaseClient, type PlApiClientConstructorOpts } from './client-base'; +import { accounts } from './client/accounts'; +import { admin } from './client/admin'; +import { announcements } from './client/announcements'; +import { antennas } from './client/antennas'; +import { apps } from './client/apps'; +import { asyncRefreshes } from './client/async-refreshes'; +import { chats } from './client/chats'; +import { circles } from './client/circles'; +import { drive } from './client/drive'; +import { emails } from './client/emails'; +import { events } from './client/events'; +import { experimental } from './client/experimental'; +import { filtering } from './client/filtering'; +import { groupedNotifications } from './client/grouped-notifications'; +import { instance } from './client/instance'; +import { interactionRequests } from './client/interaction-requests'; +import { lists } from './client/lists'; +import { media } from './client/media'; +import { myAccount } from './client/my-account'; +import { notifications } from './client/notifications'; +import { oauth } from './client/oauth'; +import { oembed } from './client/oembed'; +import { polls } from './client/polls'; +import { pushNotifications } from './client/push-notifications'; +import { rssFeedSubscriptions } from './client/rss-feed-subscriptions'; +import { scheduledStatuses } from './client/scheduled-statuses'; +import { search } from './client/search'; +import { settings } from './client/settings'; +import { shoutbox } from './client/shoutbox'; +import { statuses } from './client/statuses'; +import { stories } from './client/stories'; +import { streaming } from './client/streaming'; +import { subscriptions } from './client/subscriptions'; +import { timelines } from './client/timelines'; +import { trends } from './client/trends'; +import { utils } from './client/utils'; +import { ICESHRIMP_NET } from './features'; -import { - accountSchema, - adminAccountSchema, - adminAnnouncementSchema, - adminCanonicalEmailBlockSchema, - adminCohortSchema, - adminCustomEmojiSchema, - adminDimensionSchema, - adminDomainAllowSchema, - adminDomainBlockSchema, - adminDomainSchema, - adminEmailDomainBlockSchema, - adminIpBlockSchema, - adminMeasureSchema, - adminModerationLogEntrySchema, - adminRelaySchema, - adminReportSchema, - adminRuleSchema, - adminTagSchema, - announcementSchema, - antennaSchema, - applicationSchema, - asyncRefreshSchema, - authorizationServerMetadataSchema, - backupSchema, - blockedAccountSchema, - bookmarkFolderSchema, - chatMessageSchema, - chatSchema, - circleSchema, - contextSchema, - conversationSchema, - credentialAccountSchema, - credentialApplicationSchema, - customEmojiSchema, - domainBlockSchema, - driveFileSchema, - driveFolderSchema, - driveStatusSchema, - emojiReactionSchema, - extendedDescriptionSchema, - familiarFollowersSchema, - featuredTagSchema, - filterKeywordSchema, - filterSchema, - filterStatusSchema, - groupedNotificationsResultsSchema, - groupMemberSchema, - groupRelationshipSchema, - groupSchema, - instanceSchema, - interactionPoliciesSchema, - interactionRequestSchema, - listSchema, - locationSchema, - markersSchema, - mediaAttachmentSchema, - mutedAccountSchema, - notificationPolicySchema, - notificationRequestSchema, - notificationSchema, - oauthTokenSchema, - partialStatusSchema, - pleromaConfigSchema, - pollSchema, - privacyPolicySchema, - relationshipSchema, - reportSchema, - rssFeedSchema, - ruleSchema, - scheduledStatusSchema, - scrobbleSchema, - searchSchema, - shoutMessageSchema, - statusEditSchema, - statusSchema, - statusSourceSchema, - storyCarouselItemSchema, - storyMediaSchema, - storyProfileSchema, - streamingEventSchema, - subscriptionDetailsSchema, - subscriptionInvoiceSchema, - subscriptionOptionSchema, - suggestionSchema, - tagSchema, - termsOfServiceSchema, - tokenSchema, - translationSchema, - trendsLinkSchema, - userInfoSchema, - webPushSubscriptionSchema, -} from './entities'; -import { coerceObject, filteredArray } from './entities/utils'; -import { - AKKOMA, - type Features, - getFeatures, - GOTOSOCIAL, - ICESHRIMP_NET, - MITRA, - PIXELFED, - PLEROMA, -} from './features'; -import request, { - getAsyncRefreshHeader, - getNextLink, - getPrevLink, - type RequestBody, - type RequestMeta, -} from './request'; -import { buildFullPath } from './utils/url'; - -import type { - Account, - AdminAccount, - AdminAnnouncement, - AdminModerationLogEntry, - AdminReport, - GroupedNotificationsResults, - GroupRole, - Instance, - Notification, - NotificationGroup, - PleromaConfig, - ShoutMessage, - Status, - StreamingEvent, -} from './entities'; -import type { PlApiResponse } from './main'; -import type { - CreateScrobbleParams, - FollowAccountParams, - GetAccountEndorsementsParams, - GetAccountFavouritesParams, - GetAccountFollowersParams, - GetAccountFollowingParams, - GetAccountParams, - GetAccountStatusesParams, - GetAccountSubscribersParams, - GetRelationshipsParams, - GetScrobblesParams, - ReportAccountParams, - SearchAccountParams, -} from './params/accounts'; -import type { - AdminAccountAction, - AdminCreateAccountParams, - AdminCreateAnnouncementParams, - AdminCreateCustomEmojiParams, - AdminCreateDomainBlockParams, - AdminCreateDomainParams, - AdminCreateIpBlockParams, - AdminCreateRuleParams, - AdminDimensionKey, - AdminGetAccountsParams, - AdminGetAnnouncementsParams, - AdminGetCanonicalEmailBlocks, - AdminGetCustomEmojisParams, - AdminGetDimensionsParams, - AdminGetDomainAllowsParams, - AdminGetDomainBlocksParams, - AdminGetEmailDomainBlocksParams, - AdminGetGroupsParams, - AdminGetIpBlocksParams, - AdminGetMeasuresParams, - AdminGetModerationLogParams, - AdminGetReportsParams, - AdminGetStatusesParams, - AdminMeasureKey, - AdminPerformAccountActionParams, - AdminUpdateAnnouncementParams, - AdminUpdateCustomEmojiParams, - AdminUpdateDomainBlockParams, - AdminUpdateReportParams, - AdminUpdateRuleParams, - AdminUpdateStatusParams, -} from './params/admin'; -import type { CreateAntennaParams, UpdateAntennaParams } from './params/antennas'; -import type { CreateApplicationParams } from './params/apps'; -import type { - CreateChatMessageParams, - GetChatMessagesParams, - GetChatsParams, -} from './params/chats'; -import type { GetCircleAccountsParams, GetCircleStatusesParams } from './params/circles'; -import type { UpdateFileParams } from './params/drive'; -import type { - CreateEventParams, - EditEventParams, - GetEventParticipationRequestsParams, - GetEventParticipationsParams, - GetJoinedEventsParams, -} from './params/events'; -import type { - BlockAccountParams, - CreateFilterParams, - GetBlocksParams, - GetDomainBlocksParams, - GetMutesParams, - MuteAccountParams, - UpdateFilterParams, -} from './params/filtering'; -import type { - GetGroupedNotificationsParams, - GetUnreadNotificationGroupCountParams, -} from './params/grouped-notifications'; -import type { - CreateGroupParams, - GetGroupBlocksParams, - GetGroupMembershipRequestsParams, - GetGroupMembershipsParams, - UpdateGroupParams, -} from './params/groups'; -import type { ProfileDirectoryParams } from './params/instance'; -import type { GetInteractionRequestsParams } from './params/interaction-requests'; -import type { CreateListParams, GetListAccountsParams, UpdateListParams } from './params/lists'; -import type { UpdateMediaParams, UploadMediaParams } from './params/media'; -import type { - CreateBookmarkFolderParams, - GetBookmarksParams, - GetEndorsementsParams, - GetFavouritesParams, - GetFollowedTagsParams, - GetFollowRequestsParams, - UpdateBookmarkFolderParams, -} from './params/my-account'; -import type { - GetNotificationParams, - GetNotificationRequestsParams, - GetUnreadNotificationCountParams, - UpdateNotificationPolicyRequest, -} from './params/notifications'; -import type { - GetTokenParams, - MfaChallengeParams, - OauthAuthorizeParams, - RevokeTokenParams, -} from './params/oauth'; -import type { - CreatePushNotificationsSubscriptionParams, - UpdatePushNotificationsSubscriptionParams, -} from './params/push-notifications'; -import type { GetScheduledStatusesParams } from './params/scheduled-statuses'; -import type { SearchParams } from './params/search'; -import type { - CreateAccountParams, - UpdateCredentialsParams, - UpdateInteractionPoliciesParams, - UpdateNotificationSettingsParams, -} from './params/settings'; -import type { - CreateStatusParams, - EditInteractionPolicyParams, - EditStatusParams, - GetFavouritedByParams, - GetRebloggedByParams, - GetStatusContextParams, - GetStatusesParams, - GetStatusMentionedUsersParams, - GetStatusParams, - GetStatusQuotesParams, - GetStatusReferencesParams, -} from './params/statuses'; -import type { - CreateStoryParams, - CreateStoryPollParams, - CropStoryPhotoParams, - StoryReportType, -} from './params/stories'; -import type { - AntennaTimelineParams, - BubbleTimelineParams, - GetConversationsParams, - GroupTimelineParams, - HashtagTimelineParams, - HomeTimelineParams, - LinkTimelineParams, - ListTimelineParams, - PublicTimelineParams, - SaveMarkersParams, - WrenchedTimelineParams, -} from './params/timelines'; -import type { GetTrendingLinks, GetTrendingStatuses, GetTrendingTags } from './params/trends'; -import type { PaginatedResponse } from './responses'; - -const GROUPED_TYPES = [ - 'favourite', - 'reblog', - 'emoji_reaction', - 'event_reminder', - 'participation_accepted', - 'participation_request', -]; - -type EmptyObject = Record; - -interface PlApiClientConstructorOpts { - /** Instance object to use by default, to be populated eg. from cache */ - instance?: Instance; +interface PlApiClientFullConstructorOpts extends PlApiClientConstructorOpts { /** Fetch instance after constructing */ fetchInstance?: boolean; /** Abort signal which can be used to cancel the callbacks */ fetchInstanceSignal?: AbortSignal; /** Executed after the initial instance fetch */ - onInstanceFetchSuccess?: (instance: Instance) => void; + onInstanceFetchSuccess?: (instance: import('./entities/instance').Instance) => void; /** Executed when the initial instance fetch failed */ onInstanceFetchError?: (error?: any) => void; - /** Custom authorization token to use for requests */ - customAuthorizationToken?: string; } /** - * Mastodon API client. + * Mastodon API client with all categories. * @category Clients */ -class PlApiClient { - baseURL: string; - #accessToken?: string; - #iceshrimpAccessToken?: string; - #customAuthorizationToken?: string; - #instance: Instance = v.parse(instanceSchema, {}); - public request = request.bind(this) as typeof request; - public features: Features = getFeatures(this.#instance); - #socket?: { - listen: (listener: any, stream?: string) => number; - unlisten: (listener: any) => void; - subscribe: (stream: string, params?: { list?: string; tag?: string }) => void; - unsubscribe: (stream: string, params?: { list?: string; tag?: string }) => void; - close: () => void; - }; - #shoutSocket?: { - message: (text: string) => void; - close: () => void; - }; +class PlApiClient extends PlApiBaseClient { + readonly accounts = accounts(this); + readonly admin = admin(this); + readonly announcements = announcements(this); + readonly antennas = antennas(this); + readonly apps = apps(this); + readonly asyncRefreshes = asyncRefreshes(this); + readonly chats = chats(this); + readonly circles = circles(this); + readonly drive = drive(this); + readonly emails = emails(this); + readonly events = events(this); + readonly experimental = experimental(this); + readonly filtering = filtering(this); + readonly groupedNotifications = groupedNotifications(this); + readonly instance = instance(this); + readonly interactionRequests = interactionRequests(this); + readonly lists = lists(this); + readonly media = media(this); + readonly myAccount = myAccount(this); + readonly notifications = notifications(this); + readonly oauth = oauth(this); + readonly oembed = oembed(this); + readonly polls = polls(this); + readonly pushNotifications = pushNotifications(this); + readonly rssFeedSubscriptions = rssFeedSubscriptions(this); + readonly scheduledStatuses = scheduledStatuses(this); + readonly search = search(this); + readonly settings = settings(this); + readonly shoutbox = shoutbox(this); + readonly statuses = statuses(this); + readonly stories = stories(this); + readonly streaming = streaming(this); + readonly subscriptions = subscriptions(this); + readonly timelines = timelines(this); + readonly trends = trends(this); + readonly utils = utils(this); - /** - * - * @param baseURL Mastodon API-compatible server URL - * @param accessToken OAuth token for an authorized user - */ constructor( baseURL: string, accessToken?: string, { - instance, fetchInstance, fetchInstanceSignal, onInstanceFetchSuccess, onInstanceFetchError, - customAuthorizationToken, - }: PlApiClientConstructorOpts = {}, + ...opts + }: PlApiClientFullConstructorOpts = {}, ) { - this.baseURL = baseURL; - this.#accessToken = accessToken; - this.#customAuthorizationToken = customAuthorizationToken; + super(baseURL, accessToken, opts); - if (instance) { - this.#setInstance(instance); - } if (fetchInstance) { this.instance .getInstance() @@ -377,6882 +117,50 @@ class PlApiClient { } } - #paginatedGet = async ( - input: URL | RequestInfo, - body: RequestBody, - schema: v.BaseSchema>, - isArray = true as IsArray, - ): Promise> => { - const targetSchema = isArray ? filteredArray(schema) : schema; - - const processResponse = (response: PlApiResponse) => - ({ - previous: getMore(getPrevLink(response)), - next: getMore(getNextLink(response)), - items: v.parse(targetSchema, response.json), - partial: response.status === 206, - }) as PaginatedResponse; - - const getMore = (input: string | null) => - input ? () => this.request(input).then(processResponse) : null; - - const response = await this.request(input, body); - - return processResponse(response); - }; - - #paginatedPleromaAccounts = async (params: { - query?: string; - filters?: string; - page?: number; - page_size: number; - tags?: Array; - actor_types?: Array; - name?: string; - email?: string; - }): Promise> => { - const response = await this.request('/api/v1/pleroma/admin/users', { params }); - - const adminAccounts = v.parse(filteredArray(adminAccountSchema), response.json?.users); - // uncomment when pleroma gets /api/v1/accounts?id[] support - // const accounts = await this.accounts.getAccounts(adminAccounts.map(({ id }) => id)); - // adminAccounts.forEach((adminAccount) => adminAccount.account = accounts.find(({ id }) => id === adminAccount.id) || null); - - return { - previous: params.page - ? () => this.#paginatedPleromaAccounts({ ...params, page: params.page! - 1 }) - : null, - next: - response.json?.count > - params.page_size * ((params.page || 1) - 1) + response.json?.users?.length - ? () => this.#paginatedPleromaAccounts({ ...params, page: (params.page || 0) + 1 }) - : null, - items: adminAccounts, - partial: response.status === 206, - total: response.json?.count, - }; - }; - - #paginatedPleromaReports = async (params: { - state?: 'open' | 'closed' | 'resolved'; - limit?: number; - page?: number; - page_size: number; - }): Promise> => { - const response = await this.request('/api/v1/pleroma/admin/reports', { params }); - - return { - previous: params.page - ? () => this.#paginatedPleromaReports({ ...params, page: params.page! - 1 }) - : null, - next: - response.json?.total > - params.page_size * ((params.page || 1) - 1) + response.json?.reports?.length - ? () => this.#paginatedPleromaReports({ ...params, page: (params.page || 0) + 1 }) - : null, - items: v.parse(filteredArray(adminReportSchema), response.json?.reports), - partial: response.status === 206, - total: response.json?.total, - }; - }; - - #paginatedPleromaStatuses = async (params: { - page_size?: number; - local_only?: boolean; - godmode?: boolean; - with_reblogs?: boolean; - page?: number; - }): Promise> => { - const response = await this.request('/api/v1/pleroma/admin/statuses', { params }); - - return { - previous: params.page - ? () => this.#paginatedPleromaStatuses({ ...params, page: params.page! - 1 }) - : null, - next: response.json?.length - ? () => this.#paginatedPleromaStatuses({ ...params, page: (params.page || 0) + 1 }) - : null, - items: v.parse(filteredArray(statusSchema), response.json), - partial: response.status === 206, - }; - }; - - #paginatedIceshrimpAccountsList = async ( - url: string, - fn: (body: T) => Array, - ): Promise> => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(url); - const ids = fn(response.json); - - const accounts = await this.accounts.getAccounts(ids); - - const prevLink = getPrevLink(response); - const nextLink = getNextLink(response); - - return { - previous: prevLink ? () => this.#paginatedIceshrimpAccountsList(prevLink, fn) : null, - next: nextLink ? () => this.#paginatedIceshrimpAccountsList(nextLink, fn) : null, - items: accounts, - partial: response.status === 206, - }; - }; - - #groupNotifications = ( - { previous, next, items, ...response }: PaginatedResponse, - params?: GetGroupedNotificationsParams, - ): PaginatedResponse => { - const notificationGroups: Array = []; - - for (const notification of items) { - let existingGroup: NotificationGroup | undefined; - if ((params?.grouped_types || GROUPED_TYPES).includes(notification.type)) { - existingGroup = notificationGroups.find( - (notificationGroup) => - notificationGroup.type === notification.type && - (notification.type === 'emoji_reaction' && notificationGroup.type === 'emoji_reaction' - ? notification.emoji === notificationGroup.emoji - : true) && - // @ts-expect-error used optional chaining - notificationGroup.status_id === notification.status?.id, - ); - } - - if (existingGroup) { - existingGroup.notifications_count += 1; - existingGroup.page_min_id = notification.id; - existingGroup.sample_account_ids.push(notification.account.id); - } else { - notificationGroups.push({ - ...omit(notification, ['account', 'status', 'target']), - group_key: notification.id, - notifications_count: 1, - most_recent_notification_id: notification.id, - page_min_id: notification.id, - page_max_id: notification.id, - latest_page_notification_at: notification.created_at, - sample_account_ids: [notification.account.id], - // @ts-expect-error used optional chaining - status_id: notification.status?.id, - // @ts-expect-error used optional chaining - target_id: notification.target?.id, - }); - } - } - - const groupedNotificationsResults: GroupedNotificationsResults = { - accounts: Object.values( - items.reduce>((accounts, notification) => { - accounts[notification.account.id] = notification.account; - if ('target' in notification) accounts[notification.target.id] = notification.target; - - return accounts; - }, {}), - ), - statuses: Object.values( - items.reduce>((statuses, notification) => { - if ('status' in notification && notification.status) - statuses[notification.status.id] = notification.status; - return statuses; - }, {}), - ), - notification_groups: notificationGroups, - }; - - return { - ...response, - previous: previous ? async () => this.#groupNotifications(await previous(), params) : null, - next: next ? async () => this.#groupNotifications(await next(), params) : null, - items: groupedNotificationsResults, - }; - }; - - /** Register client applications that can be used to obtain OAuth tokens. */ - public readonly apps = { - /** - * Create an application - * Create a new application to obtain OAuth2 credentials. - * @see {@link https://docs.joinmastodon.org/methods/apps/#create} - */ - createApplication: async (params: CreateApplicationParams) => { - const response = await this.request('/api/v1/apps', { method: 'POST', body: params }); - - return v.parse(credentialApplicationSchema, response.json); - }, - - /** - * Verify your app works - * Confirm that the app’s OAuth2 credentials work. - * @see {@link https://docs.joinmastodon.org/methods/apps/#verify_credentials} - */ - verifyApplication: async () => { - const response = await this.request('/api/v1/apps/verify_credentials'); - - return v.parse(applicationSchema, response.json); - }, - }; - - public readonly oauth = { - /** - * Authorize a user - * Displays an authorization form to the user. If approved, it will create and return an authorization code, then redirect to the desired `redirect_uri`, or show the authorization code if `urn:ietf:wg:oauth:2.0:oob` was requested. The authorization code can be used while requesting a token to obtain access to user-level methods. - * @see {@link https://docs.joinmastodon.org/methods/oauth/#authorize} - */ - authorize: async (params: OauthAuthorizeParams) => { - const response = await this.request('/oauth/authorize', { params, contentType: '' }); - - return v.parse(v.string(), response.json); - }, - - /** - * Obtain a token - * Obtain an access token, to be used during API calls that are not public. - * @see {@link https://docs.joinmastodon.org/methods/oauth/#token} - */ - getToken: async (params: GetTokenParams) => { - if (this.features.version.software === ICESHRIMP_NET && params.grant_type === 'password') { - const loginResponse = ( - await this.request<{ - token: string; - }>('/api/iceshrimp/auth/login', { - method: 'POST', - body: { - username: params.username, - password: params.password, - }, - }) - ).json; - this.#iceshrimpAccessToken = loginResponse.token; - - const mastodonTokenResponse = ( - await this.request<{ - id: string; - token: string; - created_at: string; - scopes: Array; - }>('/api/iceshrimp/sessions/mastodon', { - method: 'POST', - body: { - appName: params.client_id, - scopes: params.scope?.split(' '), - flags: { - supportsHtmlFormatting: true, - autoDetectQuotes: false, - isPleroma: true, - supportsInlineMedia: true, - }, - }, - }) - ).json; - - return v.parse(tokenSchema, { - access_token: mastodonTokenResponse.token, - token_type: 'Bearer', - scope: mastodonTokenResponse.scopes.join(' '), - created_at: new Date(mastodonTokenResponse.created_at).getTime(), - id: mastodonTokenResponse.id, - }); - } - const response = await this.request('/oauth/token', { - method: 'POST', - body: params, - contentType: '', - }); - - return v.parse(tokenSchema, { scope: params.scope || '', ...response.json }); - }, - - /** - * Revoke a token - * Revoke an access token to make it no longer valid for use. - * @see {@link https://docs.joinmastodon.org/methods/oauth/#revoke} - */ - revokeToken: async (params: RevokeTokenParams) => { - const response = await this.request('/oauth/revoke', { - method: 'POST', - body: params, - contentType: '', - }); - - this.#socket?.close(); - - return response.json; - }, - - /** - * Retrieve user information - * Retrieves standardised OIDC claims about the currently authenticated user. - * see {@link https://docs.joinmastodon.org/methods/oauth/#userinfo} - */ - userinfo: async () => { - const response = await this.request('/oauth/userinfo'); - - return v.parse(userInfoSchema, response.json); - }, - - authorizationServerMetadata: async () => { - const response = await this.request('/.well-known/oauth-authorization-server'); - - return v.parse(authorizationServerMetadataSchema, response.json); - }, - - /** - * Get a new captcha - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromacaptcha} - */ - getCaptcha: async () => { - const response = await this.request('/api/pleroma/captcha'); - - return v.parse( - v.intersect([ - v.object({ - type: v.string(), - }), - v.record(v.string(), v.any()), - ]), - response.json, - ); - }, - - mfaChallenge: async (params: MfaChallengeParams) => { - const response = await this.request('/oauth/mfa/challenge', { method: 'POST', body: params }); - - return v.parse(tokenSchema, response.json); - }, - }; - - public readonly emails = { - resendConfirmationEmail: async (email: string) => { - const response = await this.request('/api/v1/emails/confirmations', { - method: 'POST', - body: { email }, - }); - - return response.json; - }, - }; - - public readonly accounts = { - /** - * Get account - * View information about a profile. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#get} - */ - getAccount: async (accountId: string, params?: GetAccountParams) => { - const response = await this.request(`/api/v1/accounts/${accountId}`, { params }); - - return v.parse(accountSchema, response.json); - }, - - /** - * Get multiple accounts - * View information about multiple profiles. - * - * Requires features{@link Features.getAccounts}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#index} - */ - getAccounts: async (accountId: string[]) => { - const response = await this.request('/api/v1/accounts', { params: { id: accountId } }); - - return v.parse(filteredArray(accountSchema), response.json); - }, - - /** - * Get account’s statuses - * Statuses posted to the given account. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#statuses} - */ - getAccountStatuses: (accountId: string, params?: GetAccountStatusesParams) => - this.#paginatedGet(`/api/v1/accounts/${accountId}/statuses`, { params }, statusSchema), - - /** - * Get account’s followers - * Accounts which follow the given account, if network is not hidden by the account owner. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#followers} - */ - getAccountFollowers: (accountId: string, params?: GetAccountFollowersParams) => - this.#paginatedGet(`/api/v1/accounts/${accountId}/followers`, { params }, accountSchema), - - /** - * Get account’s following - * Accounts which the given account is following, if network is not hidden by the account owner. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#following} - */ - getAccountFollowing: (accountId: string, params?: GetAccountFollowingParams) => - this.#paginatedGet(`/api/v1/accounts/${accountId}/following`, { params }, accountSchema), - - /** - * Subscriptions to the given user. - * - * Requires features{@link Features.subscriptions}. - */ - getAccountSubscribers: (accountId: string, params?: GetAccountSubscribersParams) => - this.#paginatedGet(`/api/v1/accounts/${accountId}/subscribers`, { params }, accountSchema), - - /** - * Get account’s featured tags - * Tags featured by this account. - * - * Requires features{@link Features.featuredTags}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#featured_tags} - */ - getAccountFeaturedTags: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/featured_tags`); - - return v.parse(filteredArray(featuredTagSchema), response.json); - }, - - /** - * Get lists containing this account - * User lists that you have added this account to. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#lists} - */ - getAccountLists: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/lists`); - - return v.parse(filteredArray(listSchema), response.json); - }, - - /** - * Get antennas containing this account - * User antennas that you have added this account to. - * Requires features{@link Features.antennas}. - */ - getAccountAntennas: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/antennas`); - - return v.parse(filteredArray(antennaSchema), response.json); - }, - - /** - * Get antennas excluding this account - * Requires features{@link Features.antennas}. - */ - getAccountExcludeAntennas: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/exclude_antennas`); - - return v.parse(filteredArray(circleSchema), response.json); - }, - - /** - * Get circles including this account - * User circles that you have added this account to. - * Requires features{@link Features.circles}. - */ - getAccountCircles: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/circles`); - - return v.parse(filteredArray(antennaSchema), response.json); - }, - - /** - * Follow account - * Follow the given account. Can also be used to update whether to show reblogs or enable notifications. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#follow} - */ - followAccount: async (accountId: string, params?: FollowAccountParams) => { - const response = await this.request(`/api/v1/accounts/${accountId}/follow`, { - method: 'POST', - body: params, - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Unfollow account - * Unfollow the given account. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#unfollow} - */ - unfollowAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/unfollow`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Remove account from followers - * Remove the given account from your followers. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#remove_from_followers} - */ - removeAccountFromFollowers: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/remove_from_followers`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Feature account on your profile - * Add the given account to the user’s featured profiles. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#pin} - */ - pinAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/pin`, { method: 'POST' }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Unfeature account from profile - * Remove the given account from the user’s featured profiles. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#unpin} - */ - unpinAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/unpin`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Set private note on profile - * Sets a private note on a user. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#note} - */ - updateAccountNote: async (accountId: string, comment: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/note`, { - method: 'POST', - body: { comment }, - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Check relationships to other accounts - * Find out whether a given account is followed, blocked, muted, etc. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#relationships} - */ - getRelationships: async (accountIds: string[], params?: GetRelationshipsParams) => { - const response = await this.request('/api/v1/accounts/relationships', { - params: { ...params, id: accountIds }, - }); - - return v.parse(filteredArray(relationshipSchema), response.json); - }, - - /** - * Find familiar followers - * Obtain a list of all accounts that follow a given account, filtered for accounts you follow. - * - * Requires features{@link Features.familiarFollowers}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#familiar_followers} - */ - getFamiliarFollowers: async (accountIds: string[]) => { - let response: any; - - if (this.features.version.software === PIXELFED) { - const settledResponse = await Promise.allSettled( - accountIds.map(async (accountId) => { - const accounts = (await this.request(`/api/v1.1/accounts/mutuals/${accountId}`)).json; - - return { - id: accountId, - accounts, - }; - }), - ); - - response = settledResponse.map((result, index) => - result.status === 'fulfilled' - ? result.value - : { - id: accountIds[index], - accounts: [], - }, - ); - } else { - response = ( - await this.request('/api/v1/accounts/familiar_followers', { params: { id: accountIds } }) - ).json; - } - - return v.parse(filteredArray(familiarFollowersSchema), response); - }, - - /** - * Search for matching accounts - * Search for matching accounts by username or display name. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#search} - */ - searchAccounts: async (q: string, params?: SearchAccountParams, meta?: RequestMeta) => { - const response = await this.request('/api/v1/accounts/search', { - ...meta, - params: { ...params, q }, - }); - - return v.parse(filteredArray(accountSchema), response.json); - }, - - /** - * Lookup account ID from Webfinger address - * Quickly lookup a username to see if it is available, skipping WebFinger resolution. - - * Requires features{@link Features.accountLookup}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#lookup} - */ - lookupAccount: async (acct: string, meta?: RequestMeta) => { - const response = await this.request('/api/v1/accounts/lookup', { ...meta, params: { acct } }); - - return v.parse(accountSchema, response.json); - }, - - /** - * File a report - * @see {@link https://docs.joinmastodon.org/methods/reports/#post} - */ - reportAccount: async (accountId: string, params: ReportAccountParams) => { - const response = await this.request('/api/v1/reports', { - method: 'POST', - body: { ...params, account_id: accountId }, - }); - - return v.parse(reportSchema, response.json); - }, - - /** - * Endorsements - * Returns endorsed accounts - * - * Requires features{@link Features.accountEndorsements}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaaccountsidendorsements} - * @see {@link https://docs.joinmastodon.org/methods/accounts/endorsements} - */ - getAccountEndorsements: (accountId: string, params?: GetAccountEndorsementsParams) => - this.#paginatedGet( - `/api/v1/${[PLEROMA].includes(this.features.version.software as string) ? 'pleroma/' : ''}accounts/${accountId}/endorsements`, - { params }, - accountSchema, - ), - - /** - * Birthday reminders - * Birthday reminders about users you follow. - * - * Requires features{@link Features.birthdays}. - */ - getBirthdays: async (day: number, month: number) => { - const response = await this.request('/api/v1/pleroma/birthdays', { params: { day, month } }); - - return v.parse(filteredArray(accountSchema), response.json); - }, - - /** - * Returns favorites timeline of any user - * - * Requires features{@link Features.publicFavourites}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaaccountsidfavourites} - */ - getAccountFavourites: (accountId: string, params?: GetAccountFavouritesParams) => - this.#paginatedGet( - `/api/v1/pleroma/accounts/${accountId}/favourites`, - { params }, - statusSchema, - ), - - /** - * Interact with profile or status from remote account - * - * Requires features{@link Features.remoteInteractions}. - * @param ap_id - Profile or status ActivityPub ID - * @param profile - Remote profile webfinger - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaremote_interaction} - */ - remoteInteraction: async (ap_id: string, profile: string) => { - const response = await this.request('/api/v1/pleroma/remote_interaction', { - method: 'POST', - body: { ap_id, profile }, - }); - - if (response.json?.error) throw response.json.error; - - return v.parse( - v.object({ - url: v.string(), - }), - response.json, - ); - }, - - /** - * Bite the given user. - * - * Requires features{@link Features.bites}. - * @see {@link https://github.com/purifetchi/Toki/blob/master/Toki/Controllers/MastodonApi/Bite/BiteController.cs} - */ - biteAccount: async (accountId: string) => { - let response; - switch (this.features.version.software) { - case ICESHRIMP_NET: - response = await this.request('/api/v1/bite', { - method: 'POST', - body: accountId, - }); - break; - default: - response = await this.request('/api/v1/bite', { - method: 'POST', - params: { id: accountId }, - }); - break; - } - - return response.json; - }, - - /** - * Requests a list of current and recent Listen activities for an account - * - * Requires features{@link Features.scrobbles} - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromaaccountsidscrobbles} - */ - getScrobbles: (accountId: string, params?: GetScrobblesParams) => - this.#paginatedGet( - `/api/v1/pleroma/accounts/${accountId}/scrobbles`, - { params }, - scrobbleSchema, - ), - - /** - * Creates a new Listen activity for an account - * - * Requires features{@link Features.scrobbles} - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromascrobble} - */ - createScrobble: async (params: CreateScrobbleParams) => { - if (params.external_link) (params as any).externalLink = params.external_link; - - const response = await this.request('/api/v1/pleroma/scrobble', { body: params }); - - return v.parse(scrobbleSchema, response.json); - }, - - /** - * Load latest activities from outbox - * - * Requires features{@link Features.loadActivities} - */ - loadActivities: async (accountId: string) => { - const response = await this.request( - `/api/v1/accounts/${accountId}/load_activities`, - { method: 'POST' }, - ); - - return response.json; - }, - }; - - public readonly myAccount = { - /** - * View bookmarked statuses - * Statuses the user has bookmarked. - * @see {@link https://docs.joinmastodon.org/methods/bookmarks/#get} - */ - getBookmarks: (params?: GetBookmarksParams) => - this.#paginatedGet( - this.features.bookmarkFoldersMultiple && params?.folder_id - ? `/api/v1/bookmark_categories/${params.folder_id}/statuses` - : '/api/v1/bookmarks', - { params }, - statusSchema, - ), - - /** - * View favourited statuses - * Statuses the user has favourited. - * @see {@link https://docs.joinmastodon.org/methods/favourites/#get} - */ - getFavourites: (params?: GetFavouritesParams) => - this.#paginatedGet('/api/v1/favourites', { params }, statusSchema), - - /** - * View pending follow requests - * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#get} - */ - getFollowRequests: (params?: GetFollowRequestsParams) => - this.#paginatedGet('/api/v1/follow_requests', { params }, accountSchema), - - /** - * View outgoing follow requests - * - * Requires features{@link Features.outgoingFollowRequests}. - */ - getOutgoingFollowRequests: (params?: GetFollowRequestsParams) => { - if (this.features.version.software === ICESHRIMP_NET) { - return this.#paginatedIceshrimpAccountsList( - '/api/iceshrimp/follow_requests/outgoing', - (response: Array<{ user: { id: string } }>) => response.map(({ user }) => user.id), - ); - } - - switch (this.features.version.software) { - case GOTOSOCIAL: - return this.#paginatedGet('/api/v1/follow_requests/outgoing', { params }, accountSchema); - - default: - return this.#paginatedGet( - '/api/v1/pleroma/outgoing_follow_requests', - { params }, - accountSchema, - ); - } - }, - - /** - * Accept follow request - * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#accept} - */ - acceptFollowRequest: async (accountId: string) => { - const response = await this.request(`/api/v1/follow_requests/${accountId}/authorize`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Reject follow request - * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#reject} - */ - rejectFollowRequest: async (accountId: string) => { - const response = await this.request(`/api/v1/follow_requests/${accountId}/reject`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * View currently featured profiles - * Accounts that the user is currently featuring on their profile. - * @see {@link https://docs.joinmastodon.org/methods/endorsements/#get} - */ - getEndorsements: (params?: GetEndorsementsParams) => - this.#paginatedGet('/api/v1/endorsements', { params }, accountSchema), - - /** - * View your featured tags - * List all hashtags featured on your profile. - * - * Requires features{@link Features.featuredTags}. - * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#get} - */ - getFeaturedTags: async () => { - const response = await this.request('/api/v1/featured_tags'); - - return v.parse(filteredArray(featuredTagSchema), response.json); - }, - - /** - * Feature a tag - * Promote a hashtag on your profile. - * - * Requires features{@link Features.featuredTags}. - * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#feature} - */ - featureTag: async (name: string) => { - const response = await this.request('/api/v1/featured_tags', { - method: 'POST', - body: { name }, - }); - - return v.parse(filteredArray(featuredTagSchema), response.json); - }, - - /** - * Unfeature a tag - * Stop promoting a hashtag on your profile. - * - * Requires features{@link Features.featuredTags}. - * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#unfeature} - */ - unfeatureTag: async (name: string) => { - const response = await this.request('/api/v1/featured_tags', { - method: 'DELETE', - body: { name }, - }); - - return response.json; - }, - - /** - * View suggested tags to feature - * Shows up to 10 recently-used tags. - * - * Requires features{@link Features.featuredTags}. - * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#suggestions} - */ - getFeaturedTagsSuggestions: async () => { - const response = await this.request('/api/v1/featured_tags/suggestions'); - - return v.parse(filteredArray(tagSchema), response.json); - }, - - /** - * View all followed tags - * List your followed hashtags. - * - * Requires features{@link Features.followHashtags}. - * @see {@link https://docs.joinmastodon.org/methods/followed_tags/#get} - */ - getFollowedTags: (params?: GetFollowedTagsParams) => - this.#paginatedGet('/api/v1/followed_tags', { params }, tagSchema), - - /** - * View information about a single tag - * Show a hashtag and its associated information - * @see {@link https://docs.joinmastodon.org/methods/tags/#get} - */ - getTag: async (tagId: string) => { - const response = await this.request(`/api/v1/tags/${tagId}`); - - return v.parse(tagSchema, response.json); - }, - - /** - * Follow a hashtag - * Follow a hashtag. Posts containing a followed hashtag will be inserted into your home timeline. - * @see {@link https://docs.joinmastodon.org/methods/tags/#follow} - */ - followTag: async (tagId: string) => { - const response = await this.request(`/api/v1/tags/${tagId}/follow`, { method: 'POST' }); - - return v.parse(tagSchema, response.json); - }, - - /** - * Unfollow a hashtag - * Unfollow a hashtag. Posts containing this hashtag will no longer be inserted into your home timeline. - * @see {@link https://docs.joinmastodon.org/methods/tags/#unfollow} - */ - unfollowTag: async (tagId: string) => { - const response = await this.request(`/api/v1/tags/${tagId}/unfollow`, { method: 'POST' }); - - return v.parse(tagSchema, response.json); - }, - - /** - * View follow suggestions - * Accounts that are promoted by staff, or that the user has had past positive interactions with, but is not yet following. - * - * Requires features{@link Features.suggestions}. - * @see {@link https://docs.joinmastodon.org/methods/suggestions/#v2} - */ - getSuggestions: async (limit?: number) => { - const response = await this.request( - this.features.version.software === PIXELFED - ? '/api/v1.1/discover/accounts/popular' - : this.features.suggestionsV2 - ? '/api/v2/suggestions' - : '/api/v1/suggestions', - { params: { limit } }, - ); - - return v.parse(filteredArray(suggestionSchema), response.json); - }, - - /** - * Remove a suggestion - * Remove an account from follow suggestions. - * - * Requires features{@link Features.suggestionsDismiss}. - * @see {@link https://docs.joinmastodon.org/methods/suggestions/#remove} - */ - dismissSuggestions: async (accountId: string) => { - const response = await this.request(`/api/v1/suggestions/${accountId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * Gets user bookmark folders - * - * Requires features{@link Features.bookmarkFolders}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromabookmark_folders} - */ - getBookmarkFolders: async () => { - const response = await this.request( - this.features.version.software === PLEROMA - ? '/api/v1/pleroma/bookmark_folders' - : '/api/v1/bookmark_categories', - ); - - return v.parse(filteredArray(bookmarkFolderSchema), response.json); - }, - - /** - * Creates a bookmark folder - * - * Requires features{@link Features.bookmarkFolders}. - * Specifying folder emoji requires features{@link Features.bookmarkFolderEmojis}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromabookmark_folders} - */ - createBookmarkFolder: async (params: CreateBookmarkFolderParams) => { - const response = await this.request( - this.features.version.software === PLEROMA - ? '/api/v1/pleroma/bookmark_folders' - : '/api/v1/bookmark_categories', - { method: 'POST', body: { title: params.name, ...params } }, - ); - - return v.parse(bookmarkFolderSchema, response.json); - }, - - /** - * Updates a bookmark folder - * - * Requires features{@link Features.bookmarkFolders}. - * Specifying folder emoji requires features{@link Features.bookmarkFolderEmojis}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#patch-apiv1pleromabookmark_foldersid} - */ - updateBookmarkFolder: async (bookmarkFolderId: string, params: UpdateBookmarkFolderParams) => { - const response = await this.request( - `${this.features.version.software === PLEROMA ? '/api/v1/pleroma/bookmark_folders' : '/api/v1/bookmark_categories'}/${bookmarkFolderId}`, - { method: 'PATCH', body: { title: params.name, ...params } }, - ); - - return v.parse(bookmarkFolderSchema, response.json); - }, - - /** - * Deletes a bookmark folder - * - * Requires features{@link Features.bookmarkFolders}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromabookmark_foldersid} - */ - deleteBookmarkFolder: async (bookmarkFolderId: string) => { - const response = await this.request( - `${this.features.version.software === PLEROMA ? '/api/v1/pleroma/bookmark_folders' : '/api/v1/bookmark_categories'}/${bookmarkFolderId}`, - { method: 'DELETE' }, - ); - - return v.parse(bookmarkFolderSchema, response.json); - }, - - /** - * Requires features{@link Features.bookmarkFoldersMultiple}. - */ - addBookmarkToFolder: async (statusId: string, folderId: string) => { - const response = await this.request( - `/api/v1/bookmark_categories/${folderId}/statuses`, - { method: 'POST', params: { status_ids: [statusId] } }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.bookmarkFoldersMultiple}. - */ - removeBookmarkFromFolder: async (statusId: string, folderId: string) => { - const response = await this.request( - `/api/v1/bookmark_categories/${folderId}/statuses`, - { method: 'DELETE', params: { status_ids: [statusId] } }, - ); - - return response.json; - }, - }; - - public readonly settings = { - /** - * Register an account - * Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox. - * - * Requires features{@link Features.accountCreation} - * @see {@link https://docs.joinmastodon.org/methods/accounts/#create} - */ - createAccount: async (params: CreateAccountParams) => { - const response = await this.request('/api/v1/accounts', { - method: 'POST', - body: { language: params.locale, birthday: params.date_of_birth, ...params }, - }); - - if ('identifier' in response.json) - return v.parse( - v.object({ - message: v.string(), - identifier: v.string(), - }), - response.json, - ); - return v.parse(tokenSchema, response.json); - }, - - /** - * Verify account credentials - * Test to make sure that the user token works. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#verify_credentials} - */ - verifyCredentials: async () => { - const response = await this.request('/api/v1/accounts/verify_credentials'); - - return v.parse(credentialAccountSchema, response.json); - }, - - /** - * Update account credentials - * Update the user’s display and preferences. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#update_credentials} - */ - updateCredentials: async (params: UpdateCredentialsParams) => { - if (params.background_image) { - (params as any).pleroma_background_image = params.background_image; - delete params.background_image; - } - - if (params.settings_store) { - (params as any).pleroma_settings_store = params.settings_store; - - if (this.features.version.software === MITRA) { - await this.request('/api/v1/settings/client_config', { - method: 'POST', - body: params.settings_store, - }); - } - - delete params.settings_store; - } - - const response = await this.request('/api/v1/accounts/update_credentials', { - method: 'PATCH', - contentType: - this.features.version.software === GOTOSOCIAL || - this.features.version.software === ICESHRIMP_NET || - params.avatar || - params.header - ? '' - : undefined, - body: params, - }); - - return v.parse(credentialAccountSchema, response.json); - }, - - /** - * Delete profile avatar - * Deletes the avatar associated with the user’s profile. - * @see {@link https://docs.joinmastodon.org/methods/profile/#delete-profile-avatar} - */ - deleteAvatar: async () => { - const response = await this.request('/api/v1/profile/avatar', { method: 'DELETE' }); - - return v.parse(credentialAccountSchema, response.json); - }, - - /** - * Delete profile header - * Deletes the header image associated with the user’s profile. - * @see {@link https://docs.joinmastodon.org/methods/profile/#delete-profile-header} - */ - deleteHeader: async () => { - const response = await this.request('/api/v1/profile/header', { method: 'DELETE' }); - - return v.parse(credentialAccountSchema, response.json); - }, - - /** - * View user preferences - * Preferences defined by the user in their account settings. - * @see {@link https://docs.joinmastodon.org/methods/preferences/#get} - */ - getPreferences: async () => { - const response = await this.request('/api/v1/preferences'); - - return response.json as Record; - }, - - /** - * Create a user backup archive - * - * Requires features{@link Features.accountBackups}. - */ - createBackup: async () => { - const response = await this.request('/api/v1/pleroma/backups', { method: 'POST' }); - - return v.parse(backupSchema, response.json); - }, - - /** - * List user backups - * - * Requires features{@link Features.accountBackups}. - */ - getBackups: async () => { - const response = await this.request('/api/v1/pleroma/backups'); - - return v.parse(filteredArray(backupSchema), response.json); - }, - - /** - * Get aliases of the current account - * - * Requires features{@link Features.manageAccountAliases}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-aliases-of-the-current-account} - */ - getAccountAliases: async () => { - const response = await this.request('/api/pleroma/aliases'); - - return v.parse(v.object({ aliases: filteredArray(v.string()) }), response.json); - }, - - /** - * Add alias to the current account - * - * Requires features{@link Features.manageAccountAliases}. - * @param alias - the nickname of the alias to add, e.g. foo@example.org. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#add-alias-to-the-current-account} - */ - addAccountAlias: async (alias: string) => { - const response = await this.request('/api/pleroma/aliases', { - method: 'PUT', - body: { alias }, - }); - - return v.parse(v.object({ status: v.literal('success') }), response.json); - }, - - /** - * Delete alias from the current account - * - * Requires features{@link Features.manageAccountAliases}. - * @param alias - the nickname of the alias to add, e.g. foo@example.org. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-alias-from-the-current-account} - */ - deleteAccountAlias: async (alias: string) => { - const response = await this.request('/api/pleroma/aliases', { - method: 'DELETE', - body: { alias }, - }); - - return v.parse(v.object({ status: v.literal('success') }), response.json); - }, - - /** - * Retrieve a list of active sessions for the user - * - * Requires features{@link Features.sessions}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} - */ - getOauthTokens: () => { - let url; - - switch (this.features.version.software) { - case GOTOSOCIAL: - url = '/api/v1/tokens'; - break; - case MITRA: - url = '/api/v1/settings/sessions'; - break; - default: - url = '/api/oauth_tokens'; - break; - } - - return this.#paginatedGet(url, {}, oauthTokenSchema); - }, - - /** - * Revoke a user session by its ID - * - * Requires features{@link Features.sessions}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apioauth_tokensid} - */ - deleteOauthToken: async (oauthTokenId: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request(`/api/v1/tokens/${oauthTokenId}/invalidate`, { - method: 'POST', - }); - break; - case MITRA: - response = await this.request(`/api/v1/settings/sessions/${oauthTokenId}`, { - method: 'DELETE', - }); - break; - default: - response = await this.request(`/api/oauth_tokens/${oauthTokenId}`, { - method: 'DELETE', - }); - break; - } - - return response.json; - }, - - /** - * Change account password - * - * Requires features{@link Features.changePassword}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger} - * @see {@link https://codeberg.org/silverpill/mitra/src/commit/f15c19527191d82bc3643f984deca43d1527525d/docs/openapi.yaml} - * @see {@link https://git.pleroma.social/pleroma/pleroma/-/blob/develop/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex?ref_type=heads#L68} - */ - changePassword: async (current_password: string, new_password: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user/password_change', { - method: 'POST', - body: { - old_password: current_password, - new_password, - }, - }); - break; - case ICESHRIMP_NET: - await this.#getIceshrimpAccessToken(); - response = await this.request('/api/iceshrimp/auth/change-password', { - method: 'POST', - body: { - oldPassword: current_password, - newPassword: new_password, - }, - }); - break; - case MITRA: - response = await this.request('/api/v1/settings/change_password', { - method: 'POST', - body: { new_password }, - }); - break; - case PIXELFED: - response = await this.request('/api/v1.1/accounts/change-password', { - method: 'POST', - body: { - current_password, - new_password, - confirm_password: new_password, - }, - }); - if (response.redirected) throw response; - break; - default: - response = await this.request('/api/pleroma/change_password', { - method: 'POST', - body: { - password: current_password, - new_password, - new_password_confirmation: new_password, - }, - }); - } - - return response.json; - }, - - /** - * Request password reset e-mail - * - * Requires features{@link Features.resetPassword}. - */ - resetPassword: async (email?: string, nickname?: string) => { - const response = await this.request('/auth/password', { - method: 'POST', - body: { email, nickname }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.changeEmail}. - */ - changeEmail: async (email: string, password: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user/email_change', { - method: 'POST', - body: { - new_email: email, - password, - }, - }); - break; - default: - response = await this.request('/api/pleroma/change_email', { - method: 'POST', - body: { - email, - password, - }, - }); - } - - if (response.json?.error) throw response.json.error; - - return response.json; - }, - - /** - * Requires features{@link Features.deleteAccount}. - */ - deleteAccount: async (password: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/accounts/delete', { - method: 'POST', - body: { password }, - }); - break; - default: - response = await this.request('/api/pleroma/delete_account', { - method: 'POST', - body: { password }, - }); - } - - if (response.json?.error) throw response.json.error; - - return response.json; - }, - - /** - * Requires features{@link Features.deleteAccountWithoutPassword}. - */ - deleteAccountWithoutPassword: async () => { - const response = await this.request('/api/v1/settings/delete_account', { - method: 'POST', - }); - - return response.json; - }, - - /** - * Disable an account - * - * Requires features{@link Features.disableAccount}. - */ - disableAccount: async (password: string) => { - const response = await this.request('/api/pleroma/disable_account', { - method: 'POST', - body: { password }, - }); - - if (response.json?.error) throw response.json.error; - - return response.json; - }, - - /** - * Requires features{@link Features.accountMoving}. - */ - moveAccount: async (target_account: string, password: string) => { - const response = await this.request('/api/pleroma/move_account', { - method: 'POST', - body: { password, target_account }, - }); - - if (response.json?.error) throw response.json.error; - - return response.json; - }, - - mfa: { - /** - * Requires features{@link Features.manageMfa}. - */ - getMfaSettings: async () => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user').then(({ json }) => ({ - settings: { - enabled: !!json?.two_factor_enabled_at, - totp: !!json?.two_factor_enabled_at, - }, - })); - break; - default: - response = (await this.request('/api/pleroma/accounts/mfa')).json; - } - - return v.parse( - v.object({ - settings: coerceObject({ - enabled: v.boolean(), - totp: v.boolean(), - }), - }), - response, - ); - }, - - /** - * Requires features{@link Features.manageMfa}. - */ - getMfaBackupCodes: async () => { - const response = await this.request('/api/pleroma/accounts/mfa/backup_codes'); - - return v.parse( - v.object({ - codes: v.array(v.string()), - }), - response.json, - ); - }, - - /** - * Requires features{@link Features.manageMfa}. - */ - getMfaSetup: async (method: 'totp') => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user/2fa/qruri').then(({ data }) => ({ - provisioning_uri: data, - key: new URL(data).searchParams.get('secret'), - })); - break; - default: - response = (await this.request(`/api/pleroma/accounts/mfa/setup/${method}`)).json; - } - - return v.parse( - v.object({ - key: v.fallback(v.string(), ''), - provisioning_uri: v.string(), - }), - response, - ); - }, - - /** - * Requires features{@link Features.manageMfa}. - */ - confirmMfaSetup: async (method: 'totp', code: string, password: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user/2fa/enable', { - method: 'POST', - body: { code }, - }); - break; - default: - response = ( - await this.request(`/api/pleroma/accounts/mfa/confirm/${method}`, { - method: 'POST', - body: { code, password }, - }) - ).json; - } - - if (response?.error) throw response.error; - - return response as EmptyObject; - }, - - /** - * Requires features{@link Features.manageMfa}. - */ - disableMfa: async (method: 'totp', password: string) => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/user/2fa/disable', { - method: 'POST', - body: { password }, - }); - break; - default: - response = await this.request(`/api/pleroma/accounts/mfa/${method}`, { - method: 'DELETE', - body: { password }, - }); - } - - if (response.json?.error) throw response.json.error; - - return response.json; - }, - }, - - /** - * Imports your follows, for example from a Mastodon CSV file. - * - * Requires features{@link Features.importFollows}. - * `overwrite` mode requires features{@link Features.importOverwrite}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromafollow_import} - */ - importFollows: async (list: File | string, mode?: 'merge' | 'overwrite') => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/import', { - method: 'POST', - body: { data: list, type: 'following', mode }, - contentType: '', - }); - break; - case MITRA: - response = await this.request('/api/v1/settings/import_follows', { - method: 'POST', - body: { follows_csv: typeof list === 'string' ? list : await list.text() }, - }); - break; - default: - response = await this.request('/api/pleroma/follow_import', { - method: 'POST', - body: { list }, - contentType: '', - }); - } - - return response.json; - }, - - /** - * Move followers from remote alias. (experimental?) - * - * Requires features{@link Features.importFollowers}. - */ - importFollowers: async (list: File | string, actorId: string) => { - const response = await this.request('/api/v1/settings/import_followers', { - method: 'POST', - body: { - from_actor_id: actorId, - followers_csv: typeof list === 'string' ? list : await list.text(), - }, - }); - - return response.json; - }, - - /** - * Imports your blocks. - * - * Requires features{@link Features.importBlocks}. - * `overwrite` mode requires features{@link Features.importOverwrite}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromablocks_import} - */ - importBlocks: async (list: File | string, mode?: 'merge' | 'overwrite') => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/import', { - method: 'POST', - body: { data: list, type: 'blocks', mode }, - contentType: '', - }); - break; - default: - response = await this.request('/api/pleroma/blocks_import', { - method: 'POST', - body: { list }, - contentType: '', - }); - } - - return response.json; - }, - - /** - * Imports your mutes. - * - * Requires features{@link Features.importMutes}. - * `overwrite` mode requires features{@link Features.importOverwrite}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromamutes_import} - */ - importMutes: async (list: File | string, mode?: 'merge' | 'overwrite') => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/import', { - method: 'POST', - body: { data: list, type: 'blocks', mode }, - contentType: '', - }); - break; - default: - response = await this.request('/api/pleroma/mutes_import', { - method: 'POST', - body: { list }, - contentType: '', - }); - } - - return response.json; - }, - - /** - * Export followers to CSV file - * - * Requires features{@link Features.exportFollowers}. - */ - exportFollowers: async () => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/exports/followers.csv', { - method: 'GET', - }); - break; - default: - response = await this.request('/api/v1/settings/export_followers', { - method: 'GET', - }); - } - - return response.data; - }, - - /** - * Export follows to CSV file - * - * Requires features{@link Features.exportFollows}. - */ - exportFollows: async () => { - let response; - - switch (this.features.version.software) { - case GOTOSOCIAL: - response = await this.request('/api/v1/exports/following.csv', { - method: 'GET', - }); - break; - default: - response = await this.request('/api/v1/settings/export_follows', { - method: 'GET', - }); - } - - return response.data; - }, - - /** - * Export lists to CSV file - * - * Requires features{@link Features.exportLists}. - */ - exportLists: async () => { - const response = await this.request('/api/v1/exports/lists.csv', { - method: 'GET', - }); - - return response.data; - }, - - /** - * Export blocks to CSV file - * - * Requires features{@link Features.exportBlocks}. - */ - exportBlocks: async () => { - const response = await this.request('/api/v1/exports/blocks.csv', { - method: 'GET', - }); - - return response.data; - }, - - /** - * Export mutes to CSV file - * - * Requires features{@link Features.exportMutes}. - */ - exportMutes: async () => { - const response = await this.request('/api/v1/exports/mutes.csv', { - method: 'GET', - }); - - return response.data; - }, - - /** - * Updates user notification settings - * - * Requires features{@link Features.muteStrangers}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromanotification_settings} - */ - updateNotificationSettings: async (params: UpdateNotificationSettingsParams) => { - const response = await this.request('/api/pleroma/notification_settings', { - method: 'PUT', - body: params, - }); - - if (response.json?.error) throw response.json.error; - - return v.parse(v.object({ status: v.string() }), response.json); - }, - - /** - * Get default interaction policies for new statuses created by you. - * - * Requires features{@link Features.interactionRequests}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - getInteractionPolicies: async () => { - const response = await this.request('/api/v1/interaction_policies/defaults'); - - return v.parse(interactionPoliciesSchema, response.json); - }, - - /** - * Update default interaction policies per visibility level for new statuses created by you. - * - * Requires features{@link Features.interactionRequests}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - updateInteractionPolicies: async (params: UpdateInteractionPoliciesParams) => { - const response = await this.request('/api/v1/interaction_policies/defaults', { - method: 'PATCH', - body: params, - }); - - return v.parse(interactionPoliciesSchema, response.json); - }, - - /** - * List frontend setting profiles - * - * Requires features{@link Features.preferredFrontends}. - */ - getAvailableFrontends: async () => { - const response = await this.request('/api/v1/akkoma/preferred_frontend/available'); - - return v.parse(v.array(v.string()), response.json); - }, - - /** - * Update preferred frontend setting - * - * Store preferred frontend in cookies - * - * Requires features{@link Features.preferredFrontends}. - */ - setPreferredFrontend: async (frontendName: string) => { - const response = await this.request('/api/v1/akkoma/preferred_frontend', { - method: 'PUT', - body: { frontend_name: frontendName }, - }); - - return v.parse(v.object({ frontend_name: v.string() }), response.json); - }, - - authorizeIceshrimp: async () => { - const response = await this.request('/api/v1/accounts/authorize_iceshrimp', { - method: 'POST', - }); - - return response.json; - }, - }; - - public readonly filtering = { - /** - * Block account - * Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline) - * @see {@link https://docs.joinmastodon.org/methods/accounts/#block} - * `duration` parameter requires features{@link Features.blocksDuration}. - */ - blockAccount: async (accountId: string, params?: BlockAccountParams) => { - const response = await this.request(`/api/v1/accounts/${accountId}/block`, { - method: 'POST', - body: params, - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Unblock account - * Unblock the given account. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#unblock} - */ - unblockAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/unblock`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Mute account - * Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline). - * - * Requires features{@link Features.mutes}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#mute} - */ - muteAccount: async (accountId: string, params?: MuteAccountParams) => { - const response = await this.request(`/api/v1/accounts/${accountId}/mute`, { - method: 'POST', - body: params, - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * Unmute account - * Unmute the given account. - * - * Requires features{@link Features.mutes}. - * @see {@link https://docs.joinmastodon.org/methods/accounts/#unmute} - */ - unmuteAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/accounts/${accountId}/unmute`, { - method: 'POST', - }); - - return v.parse(relationshipSchema, response.json); - }, - - /** - * View muted accounts - * Accounts the user has muted. - * - * Requires features{@link Features.mutes}. - * @see {@link https://docs.joinmastodon.org/methods/mutes/#get} - */ - getMutes: (params?: GetMutesParams) => - this.#paginatedGet('/api/v1/mutes', { params }, mutedAccountSchema), - - /** - * View blocked users - * @see {@link https://docs.joinmastodon.org/methods/blocks/#get} - */ - getBlocks: (params?: GetBlocksParams) => - this.#paginatedGet('/api/v1/blocks', { params }, blockedAccountSchema), - - /** - * Get domain blocks - * View domains the user has blocked. - * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#get} - */ - getDomainBlocks: (params?: GetDomainBlocksParams) => - this.#paginatedGet('/api/v1/domain_blocks', { params }, v.string()), - - /** - * Block a domain - * Block a domain to: - * - hide all public posts from it - * - hide all notifications from it - * - remove all followers from it - * - prevent following new users from it (but does not remove existing follows) - * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#block} - */ - blockDomain: async (domain: string) => { - const response = await this.request('/api/v1/domain_blocks', { - method: 'POST', - body: { domain }, - }); - - return response.json; - }, - - /** - * Unblock a domain - * Remove a domain block, if it exists in the user’s array of blocked domains. - * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#unblock} - */ - unblockDomain: async (domain: string) => { - const response = await this.request('/api/v1/domain_blocks', { - method: 'DELETE', - body: { domain }, - }); - - return response.json; - }, - - /** - * View all filters - * Obtain a list of all filter groups for the current user. - * - * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#get} - */ - getFilters: async () => { - const response = await this.request( - this.features.filtersV2 ? '/api/v2/filters' : '/api/v1/filters', - ); - - return v.parse(filteredArray(filterSchema), response.json); - }, - - /** - * View a specific filter - * Obtain a single filter group owned by the current user. - * - * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#get-one} - */ - getFilter: async (filterId: string) => { - const response = await this.request( - this.features.filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, - ); - - return v.parse(filterSchema, response.json); - }, - - /** - * Create a filter - * Create a filter group with the given parameters. - * - * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#create} - */ - createFilter: async (params: CreateFilterParams) => { - const { filtersV2 } = this.features; - const response = await this.request(filtersV2 ? '/api/v2/filters' : '/api/v1/filters', { - method: 'POST', - body: filtersV2 - ? params - : { - phrase: params.keywords_attributes[0]?.keyword, - context: params.context, - irreversible: params.filter_action === 'hide', - whole_word: params.keywords_attributes[0]?.whole_word, - expires_in: params.expires_in, - }, - }); - - return v.parse(filterSchema, response.json); - }, - - /** - * Update a filter - * Update a filter group with the given parameters. - * - * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#update} - */ - updateFilter: async (filterId: string, params: UpdateFilterParams) => { - const { filtersV2 } = this.features; - const response = await this.request( - filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, - { - method: 'PUT', - body: filtersV2 - ? params - : { - phrase: params.keywords_attributes?.[0]?.keyword, - context: params.context, - irreversible: params.filter_action === 'hide', - whole_word: params.keywords_attributes?.[0]?.whole_word, - expires_in: params.expires_in, - }, - }, - ); - - return v.parse(filterSchema, response.json); - }, - - /** - * Delete a filter - * Delete a filter group with the given id. - * - * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#delete} - */ - deleteFilter: async (filterId: string) => { - const response = await this.request( - this.features.filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - - /** - * View keywords added to a filter - * List all keywords attached to the current filter group. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-get} - */ - getFilterKeywords: async (filterId: string) => { - const response = await this.request(`/api/v2/filters/${filterId}/keywords`); - - return v.parse(filteredArray(filterKeywordSchema), response.json); - }, - - /** - * Add a keyword to a filter - * Add the given keyword to the specified filter group - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-create} - */ - addFilterKeyword: async (filterId: string, keyword: string, whole_word?: boolean) => { - const response = await this.request(`/api/v2/filters/${filterId}/keywords`, { - method: 'POST', - body: { keyword, whole_word }, - }); - - return v.parse(filterKeywordSchema, response.json); - }, - - /** - * View a single keyword - * Get one filter keyword by the given id. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-get-one} - */ - getFilterKeyword: async (filterId: string) => { - const response = await this.request(`/api/v2/filters/keywords/${filterId}`); - - return v.parse(filterKeywordSchema, response.json); - }, - - /** - * Edit a keyword within a filter - * Update the given filter keyword. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-update} - */ - updateFilterKeyword: async (filterId: string, keyword: string, whole_word?: boolean) => { - const response = await this.request(`/api/v2/filters/keywords/${filterId}`, { - method: 'PUT', - body: { keyword, whole_word }, - }); - - return v.parse(filterKeywordSchema, response.json); - }, - - /** - * Remove keywords from a filter - * Deletes the given filter keyword. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-delete} - */ - deleteFilterKeyword: async (filterId: string) => { - const response = await this.request(`/api/v2/filters/keywords/${filterId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * View all status filters - * Obtain a list of all status filters within this filter group. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-get} - */ - getFilterStatuses: async (filterId: string) => { - const response = await this.request(`/api/v2/filters/${filterId}/statuses`); - - return v.parse(filteredArray(filterStatusSchema), response.json); - }, - - /** - * Add a status to a filter group - * Add a status filter to the current filter group. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-add} - */ - addFilterStatus: async (filterId: string, statusId: string) => { - const response = await this.request(`/api/v2/filters/${filterId}/statuses`, { - method: 'POST', - body: { status_id: statusId }, - }); - - return v.parse(filterStatusSchema, response.json); - }, - - /** - * View a single status filter - * Obtain a single status filter. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-get-one} - */ - getFilterStatus: async (statusId: string) => { - const response = await this.request(`/api/v2/filters/statuses/${statusId}`); - - return v.parse(filterStatusSchema, response.json); - }, - - /** - * Remove a status from a filter group - * Remove a status filter from the current filter group. - * - * Requires features{@link Features['filtersV2']}. - * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-remove} - */ - deleteFilterStatus: async (statusId: string) => { - const response = await this.request(`/api/v2/filters/statuses/${statusId}`, { - method: 'DELETE', - }); - - return response.json; - }, - }; - - public readonly statuses = { - /** - * Post a new status - * Publish a status with the given parameters. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#create} - */ - createStatus: async (params: CreateStatusParams) => { - type ExtendedCreateStatusParams = CreateStatusParams & { - markdown?: boolean; - circle_id?: string | null; - }; - - const fixedParams: ExtendedCreateStatusParams = params; - - if ( - params.content_type === 'text/markdown' && - this.#instance.api_versions['kmyblue_markdown.fedibird.pl-api'] >= 1 - ) { - fixedParams.markdown = true; - } - if (params.visibility?.startsWith('api/v1/bookmark_categories')) { - fixedParams.circle_id = params.visibility.slice(7); - fixedParams.visibility = 'circle'; - } - if (params.quote_id && this.#instance.api_versions.mastodon >= 7) - params.quoted_status_id = params.quote_id; - else if (params.quoted_status_id && (this.#instance.api_versions.mastodon || 0) < 7) - params.quote_id = params.quoted_status_id; - - const input = - params.preview && this.features.version.software === MITRA - ? '/api/v1/statuses/preview' - : '/api/v1/statuses'; - - const response = await this.request(input, { - method: 'POST', - body: fixedParams, - }); - - if (response.json?.scheduled_at) return v.parse(scheduledStatusSchema, response.json); - return v.parse(statusSchema, response.json); - }, - - /** - * Requires features{@link Features.createStatusPreview}. - */ - previewStatus: async (params: CreateStatusParams) => { - const input = - this.features.version.software === PLEROMA || this.features.version.software === AKKOMA - ? '/api/v1/statuses' - : '/api/v1/statuses/preview'; - - if (this.features.version.software === PLEROMA || this.features.version.software === AKKOMA) { - params.preview = true; - } - - const response = await this.request(input, { - method: 'POST', - body: params, - }); - - return v.parse(v.partial(partialStatusSchema), response.json); - }, - - /** - * View a single status - * Obtain information about a status. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#get} - */ - getStatus: async (statusId: string, params?: GetStatusParams) => { - const response = await this.request(`/api/v1/statuses/${statusId}`, { params }); - - return v.parse(statusSchema, response.json); - }, - - /** - * View multiple statuses - * Obtain information about multiple statuses. - * - * Requires features{@link Features.getStatuses}. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#index} - */ - getStatuses: async (statusIds: string[], params?: GetStatusesParams) => { - const response = await this.request('/api/v1/statuses', { - params: { ...params, id: statusIds }, - }); - - return v.parse(filteredArray(statusSchema), response.json); - }, - - /** - * Delete a status - * Delete one of your own statuses. - * - * `delete_media` parameter requires features{@link Features.deleteMedia}. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#delete} - */ - deleteStatus: async (statusId: string, deleteMedia?: boolean) => { - const response = await this.request(`/api/v1/statuses/${statusId}`, { - method: 'DELETE', - params: { delete_media: deleteMedia }, - }); - - return v.parse(statusSourceSchema, response.json); - }, - - /** - * Get parent and child statuses in context - * View statuses above and below this status in the thread. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#context} - */ - getContext: async (statusId: string, params?: GetStatusContextParams) => { - const response = await this.request(`/api/v1/statuses/${statusId}/context`, { params }); - - const asyncRefreshHeader = getAsyncRefreshHeader(response); - - return { asyncRefreshHeader, ...v.parse(contextSchema, response.json) }; - }, - - /** - * Translate a status - * Translate the status content into some language. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#translate} - */ - translateStatus: async (statusId: string, lang?: string) => { - let response; - if (this.features.version.software === AKKOMA) { - response = await this.request(`/api/v1/statuses/${statusId}/translations/${lang}`); - } else { - response = await this.request(`/api/v1/statuses/${statusId}/translate`, { - method: 'POST', - body: { lang }, - }); - } - - return v.parse(translationSchema, response.json); - }, - - /** - * Translate multiple statuses into given language. - * - * Requires features{@link Features.lazyTranslations}. - */ - translateStatuses: async (statusIds: Array, lang: string) => { - const response = await this.request('/api/v1/pl/statuses/translate', { - method: 'POST', - body: { ids: statusIds, lang }, - }); - - return v.parse(filteredArray(translationSchema), response.json); - }, - - /** - * See who boosted a status - * View who boosted a given status. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#reblogged_by} - */ - getRebloggedBy: (statusId: string, params?: GetRebloggedByParams) => - this.#paginatedGet(`/api/v1/statuses/${statusId}/reblogged_by`, { params }, accountSchema), - - /** - * See who favourited a status - * View who favourited a given status. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#favourited_by} - */ - getFavouritedBy: (statusId: string, params?: GetFavouritedByParams) => - this.#paginatedGet(`/api/v1/statuses/${statusId}/favourited_by`, { params }, accountSchema), - - /** - * Favourite a status - * Add a status to your favourites list. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#favourite} - */ - favouriteStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/favourite`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Undo favourite of a status - * Remove a status from your favourites list. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unfavourite} - */ - unfavouriteStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/unfavourite`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Boost a status - * Reshare a status on your own profile. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#reblog} - * - * Specifying reblog visibility requires features{@link Features.reblogVisibility}. - */ - reblogStatus: async (statusId: string, visibility?: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/reblog`, { - method: 'POST', - body: { visibility }, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Undo boost of a status - * Undo a reshare of a status. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unreblog} - */ - unreblogStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/unreblog`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Bookmark a status - * Privately bookmark a status. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#bookmark} - */ - bookmarkStatus: async (statusId: string, folderId?: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/bookmark`, { - method: 'POST', - body: { folder_id: folderId }, - }); - - if (folderId && this.features.bookmarkFoldersMultiple) { - await this.request(`/api/v1/bookmark_categories/${folderId}/statuses`, { - method: 'POST', - params: { status_ids: [statusId] }, - }); - } - - return v.parse(statusSchema, response.json); - }, - - /** - * Undo bookmark of a status - * Remove a status from your private bookmarks. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unbookmark} - */ - unbookmarkStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/unbookmark`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Revoke a quote post - * Revoke quote authorization of status `quoting_status_id`, detaching status `id`. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#revoke_quote} - */ - revokeQuote: async (statusId: string, quotingStatusId: string) => { - const response = await this.request( - `/api/v1/statuses/${statusId}/quotes/${quotingStatusId}/revoke`, - { method: 'POST' }, - ); - - return v.parse(statusSchema, response.json); - }, - - /** - * Mute a conversation - * Do not receive notifications for the thread that this status is part of. Must be a thread in which you are a participant. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#mute} - */ - muteStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/mute`, { method: 'POST' }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Unmute a conversation - * Start receiving notifications again for the thread that this status is part of. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unmute} - */ - unmuteStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/unmute`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Pin status to profile - * Feature one of your own public statuses at the top of your profile. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#pin} - */ - pinStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/pin`, { method: 'POST' }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Unpin status from profile - * Unfeature a status from the top of your profile. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unpin} - */ - unpinStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/unpin`, { method: 'POST' }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Edit a status - * Edit a given status to change its text, sensitivity, media attachments, or poll. Note that editing a poll’s options will reset the votes. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#unpin} - */ - editStatus: async (statusId: string, params: EditStatusParams) => { - type ExtendedEditStatusParams = EditStatusParams & { - markdown?: boolean; - }; - - const fixedParams: ExtendedEditStatusParams = params; - - if ( - params.content_type === 'text/markdown' && - this.#instance.api_versions['kmyblue_markdown.fedibird.pl-api'] >= 1 - ) { - fixedParams.markdown = true; - } - - const response = await this.request(`/api/v1/statuses/${statusId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Edit a status' interaction policies - * Edit a given status to change its interaction policies. Currently, this means changing its quote approval policy. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#edit_interaction_policy} - */ - editInteractionPolicy: async (statusId: string, params: EditInteractionPolicyParams) => { - const response = await this.request(`/api/v1/statuses/${statusId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * View edit history of a status - * Get all known versions of a status, including the initial and current states. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#history} - */ - getStatusHistory: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/history`); - - return v.parse(filteredArray(statusEditSchema), response.json); - }, - - /** - * View status source - * Obtain the source properties for a status so that it can be edited. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#source} - */ - getStatusSource: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/source`); - - return v.parse(statusSourceSchema, response.json); - }, - - /** - * Get an object of emoji to account mappings with accounts that reacted to the post - * - * Requires features{@link Features.emojiReactsList}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions} - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactionsemoji} - */ - getStatusReactions: async (statusId: string, emoji?: string) => { - const apiVersions = this.#instance.api_versions; - - let response; - if ( - apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || - this.features.version.software === ICESHRIMP_NET - ) { - response = await this.request( - `/api/v1/pleroma/statuses/${statusId}/reactions${emoji ? `/${emoji}` : ''}`, - ); - } else { - if (apiVersions['emoji_reaction.fedibird.pl-api'] >= 1) { - response = await this.request(`/api/v1/statuses/${statusId}/emoji_reactioned_by`); - } else { - response = await this.request(`/api/v1/statuses/${statusId}/reactions`, { - params: { emoji }, - }); - } - response.json = response.json?.reduce((acc: Array, cur: any) => { - if (emoji && cur.name !== emoji) return acc; - - const existing = acc.find((reaction) => reaction.name === cur.name); - - if (existing) { - existing.accounts.push(cur.account); - existing.account_ids.push(cur.account.id); - existing.count += 1; - } else - acc.push({ count: 1, accounts: [cur.account], account_ids: [cur.account.id], ...cur }); - - return acc; - }, []); - } - - return v.parse(filteredArray(emojiReactionSchema), response?.json || []); - }, - - /** - * React to a post with a unicode emoji - * - * Requires features{@link Features.emojiReacts}. - * Using custom emojis requires features{@link Features.customEmojiReacts}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#put-apiv1pleromastatusesidreactionsemoji} - */ - createStatusReaction: async (statusId: string, emoji: string) => { - const apiVersions = this.#instance.api_versions; - - let response; - if ( - apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || - this.features.version.software === MITRA - ) { - response = await this.request( - `/api/v1/pleroma/statuses/${statusId}/reactions/${encodeURIComponent(emoji)}`, - { method: 'PUT' }, - ); - } else { - response = await this.request( - `/api/v1/statuses/${statusId}/react/${encodeURIComponent(emoji)}`, - { method: 'POST' }, - ); - } - - return v.parse(statusSchema, response.json); - }, - - /** - * Remove a reaction to a post with a unicode emoji - * - * Requires features{@link Features.emojiReacts}. - * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromastatusesidreactionsemoji} - */ - deleteStatusReaction: async (statusId: string, emoji: string) => { - const apiVersions = this.#instance.api_versions; - - let response; - if ( - apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || - this.features.version.software === MITRA - ) { - response = await this.request(`/api/v1/pleroma/statuses/${statusId}/reactions/${emoji}`, { - method: 'DELETE', - }); - } else { - response = await this.request( - `/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(emoji)}`, - { method: 'POST' }, - ); - } - - return v.parse(statusSchema, response.json); - }, - - /** - * View quotes for a given status - * - * Requires features{@link Features.quotePosts}. - * @see {@link https://docs.joinmastodon.org/methods/statuses/#quotes} - */ - getStatusQuotes: (statusId: string, params?: GetStatusQuotesParams) => - this.#paginatedGet( - this.#instance.api_versions.mastodon >= 7 - ? `/api/v1/statuses/${statusId}/quotes` - : `/api/v1/pleroma/statuses/${statusId}/quotes`, - { params }, - statusSchema, - ), - - /** - * Returns the list of accounts that have disliked the status as known by the current server - * - * Requires features{@link Features.statusDislikes}. - * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#get-apifriendicastatusesiddisliked_by} - */ - getDislikedBy: (statusId: string) => - this.#paginatedGet(`/api/v1/statuses/${statusId}/disliked_by`, {}, accountSchema), - - /** - * Marks the given status as disliked by this user - * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#post-apifriendicastatusesiddislike} - */ - dislikeStatus: async (statusId: string) => { - const response = await this.request(`/api/friendica/statuses/${statusId}/dislike`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Removes the dislike mark (if it exists) on this status for this user - * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#post-apifriendicastatusesidundislike} - */ - undislikeStatus: async (statusId: string) => { - const response = await this.request(`/api/friendica/statuses/${statusId}/undislike`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - getStatusReferences: (statusId: string, params?: GetStatusReferencesParams) => - this.#paginatedGet(`/api/v1/statuses/${statusId}/referred_by`, { params }, statusSchema), - - getStatusMentionedUsers: (statusId: string, params?: GetStatusMentionedUsersParams) => - this.#paginatedGet(`/api/v1/statuses/${statusId}/mentioned_by`, { params }, accountSchema), - - /** - * Load conversation from a remote server. - * - * Requires features{@link Features.loadConversation}. - */ - loadConversation: async (statusId: string) => { - const response = await this.request( - `/api/v1/statuses/${statusId}/load_conversation`, - { method: 'POST' }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.bookmarkFoldersMultiple}. - */ - getStatusBookmarkFolders: async (statusId: string) => { - const response = await this.request(`/api/v1/statuses/${statusId}/bookmark_categories`, { - method: 'GET', - }); - - return v.parse(filteredArray(bookmarkFolderSchema), response.json); - }, - }; - - public readonly media = { - /** - * Upload media as an attachment - * Creates a media attachment to be used with a new status. The full sized media will be processed asynchronously in the background for large uploads. - * @see {@link https://docs.joinmastodon.org/methods/media/#v2} - */ - uploadMedia: async (params: UploadMediaParams, meta?: RequestMeta) => { - const response = await this.request( - this.features.mediaV2 ? '/api/v2/media' : '/api/v1/media', - { ...meta, method: 'POST', body: params, contentType: '' }, - ); - - return v.parse(mediaAttachmentSchema, response.json); - }, - - /** - * Get media attachment - * Get a media attachment, before it is attached to a status and posted, but after it is accepted for processing. Use this method to check that the full-sized media has finished processing. - * @see {@link https://docs.joinmastodon.org/methods/media/#get} - */ - getMedia: async (attachmentId: string) => { - const response = await this.request(`/api/v1/media/${attachmentId}`); - - return v.parse(mediaAttachmentSchema, response.json); - }, - - /** - * Update media attachment - * Update a MediaAttachment’s parameters, before it is attached to a status and posted. - * @see {@link https://docs.joinmastodon.org/methods/media/#update} - */ - updateMedia: async (attachmentId: string, params: UpdateMediaParams) => { - const response = await this.request(`/api/v1/media/${attachmentId}`, { - method: 'PUT', - body: params, - contentType: params.thumbnail ? '' : undefined, - }); - - return v.parse(mediaAttachmentSchema, response.json); - }, - - /** - * Update media attachment - * Update a MediaAttachment’s parameters, before it is attached to a status and posted. - * - * Requires features{@link Features.deleteMedia}. - * @see {@link https://docs.joinmastodon.org/methods/media/delete} - */ - deleteMedia: async (attachmentId: string) => { - const response = await this.request(`/api/v1/media/${attachmentId}`, { - method: 'DELETE', - }); - - return response.json; - }, - }; - - public readonly polls = { - /** - * View a poll - * View a poll attached to a status. - * @see {@link https://docs.joinmastodon.org/methods/polls/#get} - */ - getPoll: async (pollId: string) => { - const response = await this.request(`/api/v1/polls/${pollId}`); - - return v.parse(pollSchema, response.json); - }, - - /** - * Vote on a poll - * Vote on a poll attached to a status. - * @see {@link https://docs.joinmastodon.org/methods/polls/#vote} - */ - vote: async (pollId: string, choices: number[]) => { - const response = await this.request(`/api/v1/polls/${pollId}/votes`, { - method: 'POST', - body: { choices }, - }); - - return v.parse(pollSchema, response.json); - }, - }; - - public readonly scheduledStatuses = { - /** - * View scheduled statuses - * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#get} - */ - getScheduledStatuses: (params?: GetScheduledStatusesParams) => - this.#paginatedGet('/api/v1/scheduled_statuses', { params }, scheduledStatusSchema), - - /** - * View a single scheduled status - * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#get-one} - */ - getScheduledStatus: async (scheduledStatusId: string) => { - const response = await this.request(`/api/v1/scheduled_statuses/${scheduledStatusId}`); - - return v.parse(scheduledStatusSchema, response.json); - }, - - /** - * Update a scheduled status’s publishing date - * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#update} - */ - updateScheduledStatus: async (scheduledStatusId: string, scheduled_at: string) => { - const response = await this.request(`/api/v1/scheduled_statuses/${scheduledStatusId}`, { - method: 'PUT', - body: { scheduled_at }, - }); - - return v.parse(scheduledStatusSchema, response.json); - }, - - /** - * Cancel a scheduled status - * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#cancel} - */ - cancelScheduledStatus: async (scheduledStatusId: string) => { - const response = await this.request( - `/api/v1/scheduled_statuses/${scheduledStatusId}`, - { - method: 'DELETE', - }, - ); - - return response.json; - }, - }; - - public readonly timelines = { - /** - * View public timeline - * View public statuses. - * @see {@link https://docs.joinmastodon.org/methods/timelines/#public} - */ - publicTimeline: (params?: PublicTimelineParams) => - this.#paginatedGet('/api/v1/timelines/public', { params }, statusSchema), - - /** - * View hashtag timeline - * View public statuses containing the given hashtag. - * @see {@link https://docs.joinmastodon.org/methods/timelines/#tag} - */ - hashtagTimeline: (hashtag: string, params?: HashtagTimelineParams) => - this.#paginatedGet(`/api/v1/timelines/tag/${hashtag}`, { params }, statusSchema), - - /** - * View home timeline - * View statuses from followed users and hashtags. - * @see {@link https://docs.joinmastodon.org/methods/timelines/#home} - */ - homeTimeline: (params?: HomeTimelineParams) => - this.#paginatedGet('/api/v1/timelines/home', { params }, statusSchema), - - /** - * View link timeline - * View public statuses containing a link to the specified currently-trending article. This only lists statuses from people who have opted in to discoverability features. - * @see {@link https://docs.joinmastodon.org/methods/timelines/#link} - */ - linkTimeline: (url: string, params?: LinkTimelineParams) => - this.#paginatedGet('/api/v1/timelines/link', { params: { ...params, url } }, statusSchema), - - /** - * View list timeline - * View statuses in the given list timeline. - * @see {@link https://docs.joinmastodon.org/methods/timelines/#list} - */ - listTimeline: (listId: string, params?: ListTimelineParams) => - this.#paginatedGet(`/api/v1/timelines/list/${listId}`, { params }, statusSchema), - - /** - * View all conversations - * @see {@link https://docs.joinmastodon.org/methods/conversations/#get} - */ - getConversations: (params?: GetConversationsParams) => - this.#paginatedGet('/api/v1/conversations', { params }, conversationSchema), - - /** - * Remove a conversation - * Removes a conversation from your list of conversations. - * @see {@link https://docs.joinmastodon.org/methods/conversations/#delete} - */ - deleteConversation: async (conversationId: string) => { - const response = await this.request(`/api/v1/conversations/${conversationId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * Mark a conversation as read - * @see {@link https://docs.joinmastodon.org/methods/conversations/#read} - */ - markConversationRead: async (conversationId: string) => { - const response = await this.request(`/api/v1/conversations/${conversationId}/read`, { - method: 'POST', - }); - - return v.parse(conversationSchema, response.json); - }, - - /** - * Get saved timeline positions - * Get current positions in timelines. - * @see {@link https://docs.joinmastodon.org/methods/markers/#get} - */ - getMarkers: async (timelines?: string[]) => { - const response = await this.request('/api/v1/markers', { params: { timeline: timelines } }); - - return v.parse(markersSchema, response.json); - }, - - /** - * Save your position in a timeline - * Save current position in timeline. - * @see {@link https://docs.joinmastodon.org/methods/markers/#create} - */ - saveMarkers: async (params: SaveMarkersParams) => { - const response = await this.request('/api/v1/markers', { method: 'POST', body: params }); - - return v.parse(markersSchema, response.json); - }, - - /** - * Requires features{@link Features.groups}. - */ - groupTimeline: (groupId: string, params?: GroupTimelineParams) => - this.#paginatedGet( - this.features.version.software === PIXELFED - ? `/api/v0/groups/${groupId}/feed` - : `/api/v1/timelines/group/${groupId}`, - { params }, - statusSchema, - ), - - /** - * Requires features{@link Features.bubbleTimeline}. - */ - bubbleTimeline: (params?: BubbleTimelineParams) => - this.#paginatedGet('/api/v1/timelines/bubble', { params }, statusSchema), - - /** - * View antenna timeline - * Requires features{@link Features.antennas}. - */ - antennaTimeline: (antennaId: string, params?: AntennaTimelineParams) => - this.#paginatedGet(`/api/v1/timelines/antenna/${antennaId}`, { params }, statusSchema), - - /** - * Requires features{@link Features.wrenchedTimeline}. - */ - wrenchedTimeline: (params?: WrenchedTimelineParams) => - this.#paginatedGet('/api/v1/pleroma/timelines/wrenched', { params }, statusSchema), - }; - - public readonly lists = { - /** - * View your lists - * Fetch all lists that the user owns. - * @see {@link https://docs.joinmastodon.org/methods/lists/#get} - */ - getLists: async () => { - const response = await this.request('/api/v1/lists'); - - return v.parse(filteredArray(listSchema), response.json); - }, - - /** - * Show a single list - * Fetch the list with the given ID. Used for verifying the title of a list, and which replies to show within that list. - * @see {@link https://docs.joinmastodon.org/methods/lists/#get-one} - */ - getList: async (listId: string) => { - const response = await this.request(`/api/v1/lists/${listId}`); - - return v.parse(listSchema, response.json); - }, - - /** - * Create a list - * Create a new list. - * @see {@link https://docs.joinmastodon.org/methods/lists/#create} - */ - createList: async (params: CreateListParams) => { - const response = await this.request('/api/v1/lists', { method: 'POST', body: params }); - - return v.parse(listSchema, response.json); - }, - - /** - * Update a list - * Change the title of a list, or which replies to show. - * @see {@link https://docs.joinmastodon.org/methods/lists/#update} - */ - updateList: async (listId: string, params: UpdateListParams) => { - const response = await this.request(`/api/v1/lists/${listId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(listSchema, response.json); - }, - - /** - * Delete a list - * @see {@link https://docs.joinmastodon.org/methods/lists/#delete} - */ - deleteList: async (listId: string) => { - const response = await this.request(`/api/v1/lists/${listId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * View accounts in a list - * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts} - */ - getListAccounts: (listId: string, params?: GetListAccountsParams) => - this.#paginatedGet(`/api/v1/lists/${listId}/accounts`, { params }, accountSchema), - - /** - * Add accounts to a list - * Add accounts to the given list. Note that the user must be following these accounts. - * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts-add} - */ - addListAccounts: async (listId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/lists/${listId}/accounts`, { - method: 'POST', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** - * Remove accounts from list - * Remove accounts from the given list. - * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts-remove} - */ - deleteListAccounts: async (listId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/lists/${listId}/accounts`, { - method: 'DELETE', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** - * Add a list to favourites - * - * Requires features{@link Features.listsFavourite}. - */ - favouriteList: async (listId: string) => { - const response = await this.request(`/api/v1/lists/${listId}/favourite`, { method: 'POST' }); - - return v.parse(listSchema, response.json); - }, - - /** - * Remove a list from favourites - * - * Requires features{@link Features.listsFavourite}. - */ - unfavouriteList: async (listId: string) => { - const response = await this.request(`/api/v1/lists/${listId}/unfavourite`, { - method: 'POST', - }); - - return v.parse(listSchema, response.json); - }, - }; - - public readonly streaming = { - /** - * Check if the server is alive - * Verify that the streaming service is alive before connecting to it - * @see {@link https://docs.joinmastodon.org/methods/streaming/#health} - */ - health: async () => { - const response = await this.request('/api/v1/streaming/health'); - - return v.parse(v.literal('OK'), response.json); - }, - - /** - * Establishing a WebSocket connection - * Open a multiplexed WebSocket connection to receive events. - * @see {@link https://docs.joinmastodon.org/methods/streaming/#websocket} - */ - connect: () => { - if (this.#socket) return this.#socket; - - const path = buildFullPath( - '/api/v1/streaming', - this.#instance?.configuration.urls.streaming, - { access_token: this.accessToken }, - ); - - const ws = new WebSocket(path, this.accessToken as any); - - let listeners: Array<{ listener: (event: StreamingEvent) => any; stream?: string }> = []; - const queue: Array<() => any> = []; - - const enqueue = (fn: () => any) => - ws.readyState === WebSocket.CONNECTING ? queue.push(fn) : fn(); - - ws.onmessage = (event) => { - const message = v.parse(streamingEventSchema, JSON.parse(event.data as string)); - - listeners.filter( - ({ listener, stream }) => - (!stream || message.stream.includes(stream)) && listener(message), - ); - }; - - ws.onopen = () => { - queue.forEach((fn) => fn()); - }; - - this.#socket = { - listen: (listener: (event: StreamingEvent) => any, stream?: string) => - listeners.push({ listener, stream }), - unlisten: (listener: (event: StreamingEvent) => any) => - (listeners = listeners.filter((value) => value.listener !== listener)), - subscribe: (stream: string, { list, tag }: { list?: string; tag?: string } = {}) => - enqueue(() => ws.send(JSON.stringify({ type: 'subscribe', stream, list, tag }))), - unsubscribe: (stream: string, { list, tag }: { list?: string; tag?: string } = {}) => - enqueue(() => ws.send(JSON.stringify({ type: 'unsubscribe', stream, list, tag }))), - close: () => { - ws.close(); - this.#socket = undefined; - }, - }; - - return this.#socket; - }, - }; - - public readonly notifications = { - /** - * Get all notifications - * Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#get} - */ - getNotifications: (params?: GetNotificationParams, meta?: RequestMeta) => { - const PLEROMA_TYPES = [ - 'chat_mention', - 'emoji_reaction', - 'report', - 'participation_accepted', - 'participation_request', - 'event_reminder', - 'event_update', - ]; - - if (params?.types) - params.types = [ - ...params.types, - ...params.types - .filter((type) => PLEROMA_TYPES.includes(type)) - .map((type) => `pleroma:${type}`), - ]; - - if (params?.exclude_types) - params.exclude_types = [ - ...params.exclude_types, - ...params.exclude_types - .filter((type) => PLEROMA_TYPES.includes(type)) - .map((type) => `pleroma:${type}`), - ]; - - return this.#paginatedGet('/api/v1/notifications', { ...meta, params }, notificationSchema); - }, - - /** - * Get a single notification - * View information about a notification with a given ID. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-one} - */ - getNotification: async (notificationId: string) => { - const response = await this.request(`/api/v1/notifications/${notificationId}`); - - return v.parse(notificationSchema, response.json); - }, - - /** - * Dismiss all notifications - * Clear all notifications from the server. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#clear} - */ - dismissNotifications: async () => { - const response = await this.request('/api/v1/notifications/clear', { - method: 'POST', - }); - - return response.json; - }, - - /** - * Dismiss a single notification - * Dismiss a single notification from the server. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss} - */ - dismissNotification: async (notificationId: string) => { - const response = await this.request( - `/api/v1/notifications/${notificationId}/dismiss`, - { - method: 'POST', - }, - ); - - return response.json; - }, - - /** - * Get the number of unread notification - * Get the (capped) number of unread notifications for the current user. - * - * Requires features{@link Features.notificationsGetUnreadCount}. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#unread-count} - */ - getUnreadNotificationCount: async (params?: GetUnreadNotificationCountParams) => { - const response = await this.request('/api/v1/notifications/unread_count', { params }); - - return v.parse( - v.object({ - count: v.number(), - }), - response.json, - ); - }, - - /** - * Get the filtering policy for notifications - * Notifications filtering policy for the user. - * - * Requires features{@link Features.notificationsPolicy}. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-policy} - */ - getNotificationPolicy: async () => { - const response = await this.request('/api/v2/notifications/policy'); - - return v.parse(notificationPolicySchema, response.json); - }, - - /** - * Update the filtering policy for notifications - * Update the user’s notifications filtering policy. - * - * Requires features{@link Features.notificationsPolicy}. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications} - */ - updateNotificationPolicy: async (params: UpdateNotificationPolicyRequest) => { - const response = await this.request('/api/v2/notifications/policy', { - method: 'PATCH', - body: params, - }); - - return v.parse(notificationPolicySchema, response.json); - }, - - /** - * Get all notification requests - * Notification requests for notifications filtered by the user’s policy. This API returns Link headers containing links to the next/previous page. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-requests} - */ - getNotificationRequests: (params?: GetNotificationRequestsParams) => - this.#paginatedGet('/api/v1/notifications/requests', { params }, notificationRequestSchema), - - /** - * Get a single notification request - * View information about a notification request with a given ID. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-one-request} - */ - getNotificationRequest: async (notificationRequestId: string) => { - const response = await this.request( - `/api/v1/notifications/requests/${notificationRequestId}`, - ); - - return v.parse(notificationRequestSchema, response.json); - }, - - /** - * Accept a single notification request - * Accept a notification request, which merges the filtered notifications from that user back into the main notification and accepts any future notification from them. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#accept-request} - */ - acceptNotificationRequest: async (notificationRequestId: string) => { - const response = await this.request( - `/api/v1/notifications/requests/${notificationRequestId}/dismiss`, - { method: 'POST' }, - ); - - return response.json; - }, - - /** - * Dismiss a single notification request - * Dismiss a notification request, which hides it and prevent it from contributing to the pending notification requests count. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss-request} - */ - dismissNotificationRequest: async (notificationRequestId: string) => { - const response = await this.request( - `/api/v1/notifications/requests/${notificationRequestId}/dismiss`, - { method: 'POST' }, - ); - - return response.json; - }, - - /** - * Accept multiple notification requests - * Accepts multiple notification requests, which merges the filtered notifications from those users back into the main notifications and accepts any future notification from them. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests} - * Requires features{@link Features.notificationsRequestsAcceptMultiple}. - */ - acceptMultipleNotificationRequests: async (notificationRequestIds: Array) => { - const response = await this.request('/api/v1/notifications/requests/accept', { - method: 'POST', - body: { id: notificationRequestIds }, - }); - - return response.json; - }, - - /** - * Dismiss multiple notification requests - * Dismiss multiple notification requests, which hides them and prevent them from contributing to the pending notification requests count. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests} - * Requires features{@link Features.notificationsRequestsAcceptMultiple}. - */ - dismissMultipleNotificationRequests: async (notificationRequestIds: Array) => { - const response = await this.request('/api/v1/notifications/requests/dismiss', { - method: 'POST', - body: { id: notificationRequestIds }, - }); - - return response.json; - }, - - /** - * Check if accepted notification requests have been merged - * Check whether accepted notification requests have been merged. Accepting notification requests schedules a background job to merge the filtered notifications back into the normal notification list. When that process has finished, the client should refresh the notifications list at its earliest convenience. This is communicated by the `notifications_merged` streaming event but can also be polled using this endpoint. - * @see {@link https://docs.joinmastodon.org/methods/notifications/#requests-merged} - * Requires features{@link Features.notificationsRequestsAcceptMultiple}. - */ - checkNotificationRequestsMerged: async () => { - const response = await this.request('/api/v1/notifications/requests/merged'); - - return v.parse( - v.object({ - merged: v.boolean(), - }), - response.json, - ); - }, - - /** - * An endpoint to delete multiple statuses by IDs. - * - * Requires features{@link Features.notificationsDismissMultiple}. - * @see {@link https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#delete-apiv1notificationsdestroy_multiple} - */ - dismissMultipleNotifications: async (notificationIds: string[]) => { - const response = await this.request('/api/v1/notifications/destroy_multiple', { - params: { ids: notificationIds }, - method: 'DELETE', - }); - - return response.json; - }, - }; - - /** - * It is recommended to only use this with features{@link Features.groupedNotifications} available. However, there is a fallback that groups the notifications client-side. - */ - public readonly groupedNotifications = { - /** - * Get all grouped notifications - * Return grouped notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values. - * - * Requires features{@link Features.groupedNotifications}. - * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped} - */ - getGroupedNotifications: async (params: GetGroupedNotificationsParams, meta?: RequestMeta) => { - if (this.features.groupedNotifications) { - return this.#paginatedGet( - '/api/v2/notifications', - { ...meta, params }, - groupedNotificationsResultsSchema, - false, - ); - } - - const response = await this.notifications.getNotifications( - pick(params, [ - 'max_id', - 'since_id', - 'limit', - 'min_id', - 'types', - 'exclude_types', - 'account_id', - 'include_filtered', - ]), - ); - - return this.#groupNotifications(response, params); - }, - - /** - * Get a single notification group - * View information about a specific notification group with a given group key. - * - * Requires features{@link Features.groupedNotifications}. - * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group} - */ - getNotificationGroup: async (groupKey: string) => { - if (this.features.groupedNotifications) { - const response = await this.request(`/api/v2/notifications/${groupKey}`); - - return v.parse(groupedNotificationsResultsSchema, response.json); - } - - const response = await this.request(`/api/v1/notifications/${groupKey}`); - - return this.#groupNotifications({ - previous: null, - next: null, - items: [response.json], - partial: false, - }).items; - }, - - /** - * Dismiss a single notification group - * Dismiss a single notification group from the server. - * - * Requires features{@link Features.groupedNotifications}. - * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group} - */ - dismissNotificationGroup: async (groupKey: string) => { - if (this.features.groupedNotifications) { - const response = await this.request( - `/api/v2/notifications/${groupKey}/dismiss`, - { - method: 'POST', - }, - ); - - return response.json; - } - - return this.notifications.dismissNotification(groupKey); - }, - - /** - * Get accounts of all notifications in a notification group - * - * Requires features{@link Features.groupedNotifications}. - * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts} - */ - getNotificationGroupAccounts: async (groupKey: string) => { - if (this.features.groupedNotifications) { - const response = await this.request(`/api/v2/notifications/${groupKey}/accounts`); - - return v.parse(filteredArray(accountSchema), response.json); - } - - return (await this.groupedNotifications.getNotificationGroup(groupKey)).accounts; - }, - - /** - * Get the number of unread notifications - * Get the (capped) number of unread notification groups for the current user. A notification is considered unread if it is more recent than the notifications read marker. Because the count is dependant on the parameters, it is computed every time and is thus a relatively slow operation (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped. - * - * Requires features{@link Features.groupedNotifications}. - * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count} - */ - getUnreadNotificationGroupCount: async (params: GetUnreadNotificationGroupCountParams) => { - if (this.features.groupedNotifications) { - const response = await this.request('/api/v2/notifications/unread_count', { params }); - - return v.parse( - v.object({ - count: v.number(), - }), - response.json, - ); - } - - return this.notifications.getUnreadNotificationCount( - pick(params || {}, [ - 'max_id', - 'since_id', - 'limit', - 'min_id', - 'types', - 'exclude_types', - 'account_id', - ]), - ); - }, - }; - - public readonly pushNotifications = { - /** - * Subscribe to push notifications - * Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted. - * @see {@link https://docs.joinmastodon.org/methods/push/#create} - */ - createSubscription: async (params: CreatePushNotificationsSubscriptionParams) => { - const response = await this.request('/api/v1/push/subscription', { - method: 'POST', - body: params, - }); - - return v.parse(webPushSubscriptionSchema, response.json); - }, - - /** - * Get current subscription - * View the PushSubscription currently associated with this access token. - * @see {@link https://docs.joinmastodon.org/methods/push/#get} - */ - getSubscription: async () => { - const response = await this.request('/api/v1/push/subscription'); - - return v.parse(webPushSubscriptionSchema, response.json); - }, - - /** - * Change types of notifications - * Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead. - * @see {@link https://docs.joinmastodon.org/methods/push/#update} - */ - updateSubscription: async (params: UpdatePushNotificationsSubscriptionParams) => { - const response = await this.request('/api/v1/push/subscription', { - method: 'PUT', - body: params, - }); - - return v.parse(webPushSubscriptionSchema, response.json); - }, - - /** - * Remove current subscription - * Removes the current Web Push API subscription. - * @see {@link https://docs.joinmastodon.org/methods/push/#delete} - */ - deleteSubscription: async () => { - const response = await this.request('/api/v1/push/subscription', { - method: 'DELETE', - }); - - return response.json; - }, - }; - - public readonly search = { - /** - * Perform a search - * @see {@link https://docs.joinmastodon.org/methods/search/#v2} - */ - search: async (q: string, params?: SearchParams, meta?: RequestMeta) => { - const response = await this.request('/api/v2/search', { ...meta, params: { ...params, q } }); - - const parsedSearch = v.parse(searchSchema, response.json); - - // A workaround for Pleroma/Akkoma getting into a loop of returning the same account/status when resolve === true. - if (params && params.resolve && params.offset && params.offset > 0) { - const firstAccount = parsedSearch.accounts[0]; - if (firstAccount && [firstAccount.url, firstAccount.acct].includes(q)) { - parsedSearch.accounts = parsedSearch.accounts.slice(1); - } - const firstStatus = parsedSearch.statuses[0]; - if (firstStatus && [firstStatus.uri, firstStatus.url].includes(q)) { - parsedSearch.statuses = parsedSearch.statuses.slice(1); - } - } - - return parsedSearch; - }, - - /** - * Searches for locations - * - * Requires features{@link Features.events}. - * @see {@link https://github.com/mkljczk/pl/blob/fork/docs/development/API/pleroma_api.md#apiv1pleromasearchlocation} - */ - searchLocation: async (q: string, meta?: RequestMeta) => { - const response = await this.request('/api/v1/pleroma/search/location', { - ...meta, - params: { q }, - }); - - return v.parse(filteredArray(locationSchema), response.json); - }, - }; - - public readonly instance = { - /** - * View server information - * Obtain general information about the server. - * @see {@link https://docs.joinmastodon.org/methods/instance/#v2} - */ - getInstance: async () => { - let response; - try { - response = await this.request('/api/v2/instance'); - } catch (e) { - response = await this.request('/api/v1/instance'); - } - - const instance = v.parse(v.pipe(instanceSchema, v.readonly()), response.json); - this.#setInstance(instance); - - return instance; - }, - - /** - * List of connected domains - * Domains that this instance is aware of. - * @see {@link https://docs.joinmastodon.org/methods/instance/#peers} - */ - getInstancePeers: async () => { - const response = await this.request('/api/v1/instance/peers'); - - return v.parse(v.array(v.string()), response.json); - }, - - /** - * Weekly activity - * Instance activity over the last 3 months, binned weekly. - * @see {@link https://docs.joinmastodon.org/methods/instance/#activity} - */ - getInstanceActivity: async () => { - const response = await this.request('/api/v1/instance/activity'); - - return v.parse( - v.array( - v.object({ - week: v.string(), - statuses: v.pipe(v.unknown(), v.transform(String)), - logins: v.pipe(v.unknown(), v.transform(String)), - registrations: v.pipe(v.unknown(), v.transform(String)), - }), - ), - response.json, - ); - }, - - /** - * List of rules - * Rules that the users of this service should follow. - * @see {@link https://docs.joinmastodon.org/methods/instance/#rules} - */ - getInstanceRules: async () => { - const response = await this.request('/api/v1/instance/rules'); - - return v.parse(filteredArray(ruleSchema), response.json); - }, - - /** - * View moderated servers - * Obtain a list of domains that have been blocked. - * @see {@link https://docs.joinmastodon.org/methods/instance/#domain_blocks} - */ - getInstanceDomainBlocks: async () => { - const response = await this.request('/api/v1/instance/rules'); - - return v.parse(filteredArray(domainBlockSchema), response.json); - }, - - /** - * View extended description - * Obtain an extended description of this server - * @see {@link https://docs.joinmastodon.org/methods/instance/#extended_description} - */ - getInstanceExtendedDescription: async () => { - const response = await this.request('/api/v1/instance/extended_description'); - - return v.parse(extendedDescriptionSchema, response.json); - }, - - /** - * View translation languages - * Translation language pairs supported by the translation engine used by the server. - * @see {@link https://docs.joinmastodon.org/methods/instance/#translation_languages} - */ - getInstanceTranslationLanguages: async () => { - if (this.features.version.software === AKKOMA) { - const response = await this.request<{ - source: Array<{ code: string; name: string }>; - target: Array<{ code: string; name: string }>; - }>('/api/v1/akkoma/translation/languages'); - - return Object.fromEntries( - response.json.source.map((source) => [ - source.code.toLocaleLowerCase(), - response.json.target - .map((lang) => lang.code) - .filter((lang) => lang !== source.code) - .map((lang) => lang.toLocaleLowerCase()), - ]), - ); - } - - const response = await this.request('/api/v1/instance/translation_languages'); - - return v.parse(v.record(v.string(), v.array(v.string())), response.json); - }, - - /** - * View profile directory - * List accounts visible in the directory. - * @see {@link https://docs.joinmastodon.org/methods/directory/#get} - * - * Requires features{@link Features.profileDirectory}. - */ - profileDirectory: async (params?: ProfileDirectoryParams) => { - const response = await this.request('/api/v1/directory', { params }); - - return v.parse(filteredArray(accountSchema), response.json); - }, - - /** - * View all custom emoji - * Returns custom emojis that are available on the server. - * @see {@link https://docs.joinmastodon.org/methods/custom_emojis/#get} - */ - getCustomEmojis: async () => { - const response = await this.request('/api/v1/custom_emojis'); - - return v.parse(filteredArray(customEmojiSchema), response.json); - }, - - /** - * Dump frontend configurations - * - * Requires features{@link Features.frontendConfigurations}. - */ - getFrontendConfigurations: async () => { - let response; - - switch (this.features.version.software) { - case MITRA: - response = (await this.request('/api/v1/accounts/verify_credentials')).json - ?.client_config; - break; - default: - response = (await this.request('/api/pleroma/frontend_configurations')).json; - } - - return v.parse(v.fallback(v.record(v.string(), v.record(v.string(), v.any())), {}), response); - }, - - /** - * View privacy policy - * Obtain the contents of this server's privacy policy. - * @see {@link https://docs.joinmastodon.org/methods/instance/privacy_policy} - */ - getInstancePrivacyPolicy: async () => { - const response = await this.request('/api/v1/instance/privacy_policy'); - - return v.parse(privacyPolicySchema, response.json); - }, - - /** - * View terms of service - * Obtain the contents of this server's terms of service, if configured. - * @see {@link https://docs.joinmastodon.org/methods/instance/terms_of_service} - */ - getInstanceTermsOfService: async () => { - const response = await this.request('/api/v1/instance/terms_of_service'); - - return v.parse(termsOfServiceSchema, response.json); - }, - - /** - * View a specific version of the terms of service - * Obtain the contents of this server's terms of service, for a specified date, if configured. - * @see {@link https://docs.joinmastodon.org/methods/instance/terms_of_service_date} - */ - getInstanceTermsOfServiceForDate: async (date: string) => { - const response = await this.request(`/api/v1/instance/terms_of_service/${date}`); - - return v.parse(termsOfServiceSchema, response.json); - }, - }; - - public readonly trends = { - /** - * View trending tags - * Tags that are being used more frequently within the past week. - * @see {@link https://docs.joinmastodon.org/methods/trends/#tags} - */ - getTrendingTags: async (params?: GetTrendingTags) => { - const response = await this.request( - this.features.version.software === PIXELFED - ? '/api/v1.1/discover/posts/hashtags' - : '/api/v1/trends/tags', - { params }, - ); - - return v.parse(filteredArray(tagSchema), response.json); - }, - - /** - * View trending statuses - * Statuses that have been interacted with more than others. - * @see {@link https://docs.joinmastodon.org/methods/trends/#statuses} - */ - getTrendingStatuses: async (params?: GetTrendingStatuses) => { - const response = await this.request( - this.features.version.software === PIXELFED - ? '/api/pixelfed/v2/discover/posts/trending' - : '/api/v1/trends/statuses', - { params }, - ); - - return v.parse(filteredArray(statusSchema), response.json); - }, - - /** - * View trending links - * Links that have been shared more than others. - * @see {@link https://docs.joinmastodon.org/methods/trends/#links} - */ - getTrendingLinks: async (params?: GetTrendingLinks) => { - const response = await this.request('/api/v1/trends/links', { params }); - - return v.parse(filteredArray(trendsLinkSchema), response.json); - }, - }; - - public readonly announcements = { - /** - * View all announcements - * See all currently active announcements set by admins. - * @see {@link https://docs.joinmastodon.org/methods/announcements/#get} - */ - getAnnouncements: async () => { - const response = await this.request('/api/v1/announcements'); - - return v.parse(filteredArray(announcementSchema), response.json); - }, - - /** - * Dismiss an announcement - * Allows a user to mark the announcement as read. - * @see {@link https://docs.joinmastodon.org/methods/announcements/#dismiss} - */ - dismissAnnouncements: async (announcementId: string) => { - const response = await this.request(`/api/v1/announcements/${announcementId}`, { - method: 'POST', - }); - - return response.json; - }, - - /** - * Add a reaction to an announcement - * React to an announcement with an emoji. - * @see {@link https://docs.joinmastodon.org/methods/announcements/#put-reactions} - */ - addAnnouncementReaction: async (announcementId: string, emoji: string) => { - const response = await this.request( - `/api/v1/announcements/${announcementId}/reactions/${emoji}`, - { method: 'PUT' }, - ); - - return response.json; - }, - - /** - * Remove a reaction from an announcement - * Undo a react emoji to an announcement. - * @see {@link https://docs.joinmastodon.org/methods/announcements/#delete-reactions} - */ - deleteAnnouncementReaction: async (announcementId: string, emoji: string) => { - const response = await this.request( - `/api/v1/announcements/${announcementId}/reactions/${emoji}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - }; - - /** Experimental async refreshes API methods */ - public readonly asyncRefreshes = { - /** - * Get Status of Async Refresh - * @see {@link https://docs.joinmastodon.org/methods/async_refreshes/#show} - */ - show: async (id: string) => { - const response = await this.request(`/api/v1_alpha/async_refreshes/${id}`); - - return v.parse(asyncRefreshSchema, response.json); - }, - }; - - public readonly admin = { - /** Perform moderation actions with accounts. */ - accounts: { - /** - * View accounts - * View all accounts, optionally matching certain criteria for filtering, up to 100 at a time. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#v2} - */ - getAccounts: (params?: AdminGetAccountsParams) => { - if (this.features.mastodonAdminV2) { - return this.#paginatedGet('/api/v2/admin/accounts', { params }, adminAccountSchema); - } - - return this.#paginatedPleromaAccounts( - params - ? { - query: params.username, - name: params.display_name, - email: params.email, - filters: [ - params.origin === 'local' && 'local', - params.origin === 'remote' && 'external', - params.status === 'active' && 'active', - params.status === 'pending' && 'need_approval', - params.status === 'disabled' && 'deactivated', - params.permissions === 'staff' && 'is_admin', - params.permissions === 'staff' && 'is_moderator', - ] - .filter((filter) => filter) - .join(','), - page_size: 100, - } - : { page_size: 100 }, - ); - }, - - /** - * View a specific account - * View admin-level information about the given account. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#get-one} - */ - getAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}`); - } else { - response = await this.request(`/api/v1/pleroma/admin/users/${accountId}`); - } - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Approve a pending account - * Approve the given local account if it is currently pending approval. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#approve} - */ - approveAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}/approve`, { - method: 'POST', - }); - } else { - const account = await this.admin.accounts.getAccount(accountId)!; - - response = await this.request('/api/v1/pleroma/admin/users/approve', { - method: 'PATCH', - body: { nicknames: [account.username] }, - }); - response.json = response.json?.users?.[0]; - } - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Reject a pending account - * Reject the given local account if it is currently pending approval. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#reject} - */ - rejectAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}/reject`, { - method: 'POST', - }); - } else { - const account = await this.admin.accounts.getAccount(accountId)!; - - response = await this.request('/api/v1/pleroma/admin/users', { - method: 'DELETE', - body: { - nicknames: [account.username], - }, - }); - } - - return v.safeParse(adminAccountSchema, response.json).output || {}; - }, - - /** - * Requires features{@link Features.pleromaAdminAccounts}. - */ - createAccount: async (params: AdminCreateAccountParams) => { - const response = await this.request('/api/v1/pleroma/admin/users', { - method: 'POST', - body: { users: [params] }, - }); - - return v.parse( - v.object({ - nickname: v.string(), - email: v.string(), - }), - response.json[0]?.data, - ); - }, - - /** - * Delete an account - * Permanently delete data for a suspended accountusers - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#delete} - */ - deleteAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin || this.features.version.software === MITRA) { - response = await this.request(`/api/v1/admin/accounts/${accountId}`, { - method: 'DELETE', - }); - } else { - const account = await this.admin.accounts.getAccount(accountId)!; - - response = await this.request('/api/v1/pleroma/admin/users', { - method: 'DELETE', - body: { - nicknames: [account.username], - }, - }); - } - - return v.safeParse(adminAccountSchema, response.json).output || {}; - }, - - /** - * Perform an action against an account - * Perform an action against an account and log this action in the moderation history. Also resolves any open reports against this account. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#action} - */ - performAccountAction: async ( - accountId: string, - type: AdminAccountAction, - params?: AdminPerformAccountActionParams, - ) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}/action`, { - body: { ...params, type }, - }); - } else { - const account = await this.admin.accounts.getAccount(accountId)!; - - switch (type) { - case 'disable': - case 'suspend': - response = await this.request('/api/v1/pleroma/admin/users/deactivate', { - body: { nicknames: [account.username] }, - }); - break; - default: - response = { json: {} }; - break; - } - if (params?.report_id) await this.admin.reports.resolveReport(params.report_id); - } - - return response.json; - }, - - /** - * Enable a currently disabled account - * Re-enable a local account whose login is currently disabled. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#enable} - */ - enableAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}/enable`, { - method: 'POST', - }); - } else { - const account = await this.admin.accounts.getAccount(accountId)!; - response = await this.request('/api/v1/pleroma/admin/users/activate', { - method: 'PATCH', - body: { nicknames: [account.username] }, - }); - response.json = response.json?.users?.[0]; - } - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Unsilence an account - * Unsilence an account if it is currently silenced. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsilence} - */ - unsilenceAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/admin/accounts/${accountId}/unsilence`, { - method: 'POST', - }); - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Unsuspend an account - * Unsuspend a currently suspended account. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsuspend} - */ - unsuspendAccount: async (accountId: string) => { - let response; - - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/accounts/${accountId}/unsuspend`, { - method: 'POST', - }); - } else { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - response = await this.request('/api/v1/pleroma/admin/users/activate', { - method: 'PATCH', - body: { nicknames: [account!.acct] }, - }); - response.json = response.json?.users?.[0]; - } - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Unmark an account as sensitive - * Stops marking an account’s posts as sensitive, if it was previously flagged as sensitive. - * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsensitive} - */ - unsensitiveAccount: async (accountId: string) => { - const response = await this.request(`/api/v1/admin/accounts/${accountId}/unsensitive`, { - method: 'POST', - }); - - return v.parse(adminAccountSchema, response.json); - }, - - /** - * Requires features{@link Features.pleromaAdminAccounts}. - */ - promoteToAdmin: async (accountId: string) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - await this.request('/api/v1/pleroma/admin/users/permission_group/moderator', { - method: 'DELETE', - body: { nicknames: [account!.acct] }, - }); - const response = await this.request( - '/api/v1/pleroma/admin/users/permission_group/admin', - { - method: 'POST', - body: { nicknames: [account!.acct] }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.pleromaAdminAccounts}. - */ - promoteToModerator: async (accountId: string) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - await this.request('/api/v1/pleroma/admin/users/permission_group/admin', { - method: 'DELETE', - body: { nicknames: [account!.acct] }, - }); - const response = await this.request( - '/api/v1/pleroma/admin/users/permission_group/moderator', - { - method: 'POST', - body: { nicknames: [account!.acct] }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.pleromaAdminAccounts}. - */ - demoteToUser: async (accountId: string) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - await this.request('/api/v1/pleroma/admin/users/permission_group/moderator', { - method: 'DELETE', - body: { nicknames: [account!.acct] }, - }); - const response = await this.request( - '/api/v1/pleroma/admin/users/permission_group/admin', - { - method: 'DELETE', - body: { nicknames: [account!.acct] }, - }, - ); - - return response.json; - }, - - /** - * Tag a user. - * - * Requires features{@link Features.pleromaAdminAccounts}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminuserssuggest} - */ - suggestUser: async (accountId: string) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - const response = await this.request('/api/v1/pleroma/admin/users/suggest', { - method: 'PATCH', - body: { nicknames: [account!.acct] }, - }); - - return response.json; - }, - - /** - * Untag a user. - * - * Requires features{@link Features.pleromaAdminAccounts}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminusersunsuggest} - */ - unsuggestUser: async (accountId: string) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - const response = await this.request('/api/v1/pleroma/admin/users/unsuggest', { - method: 'PATCH', - body: { nicknames: [account!.acct] }, - }); - - return response.json; - }, - - /** - * Tag a user. - * - * Requires features{@link Features.pleromaAdminAccounts}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#put-apiv1pleromaadminuserstag} - */ - tagUser: async (accountId: string, tags: Array) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - const response = await this.request('/api/v1/pleroma/admin/users/tag', { - method: 'PUT', - body: { nicknames: [account!.acct], tags }, - }); - - return response.json; - }, - - /** - * Untag a user. - * - * Requires features{@link Features.pleromaAdminAccounts}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminuserstag} - */ - untagUser: async (accountId: string, tags: Array) => { - const { account } = await this.admin.accounts.getAccount(accountId)!; - - const response = await this.request('/api/v1/pleroma/admin/users/tag', { - method: 'DELETE', - body: { nicknames: [account!.acct], tags }, - }); - - return response.json; - }, - }, - - /** Disallow certain domains to federate. */ - domainBlocks: { - /** - * List all blocked domains - * Show information about all blocked domains. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#get} - */ - getDomainBlocks: (params?: AdminGetDomainBlocksParams) => - this.#paginatedGet('/api/v1/admin/domain_blocks', { params }, adminDomainBlockSchema), - - /** - * Get a single blocked domain - * Show information about a single blocked domain. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#get-one} - */ - getDomainBlock: async (domainBlockId: string) => { - const response = await this.request(`/api/v1/admin/domain_blocks/${domainBlockId}`); - - return v.parse(adminDomainBlockSchema, response.json); - }, - - /** - * Block a domain from federating - * Add a domain to the list of domains blocked from federating. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#create} - */ - createDomainBlock: async (domain: string, params?: AdminCreateDomainBlockParams) => { - const response = await this.request('/api/v1/admin/domain_blocks', { - method: 'POST', - body: { ...params, domain }, - }); - - return v.parse(adminDomainBlockSchema, response.json); - }, - - /** - * Update a domain block - * Change parameters for an existing domain block. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#update} - */ - updateDomainBlock: async (domainBlockId: string, params?: AdminUpdateDomainBlockParams) => { - const response = await this.request(`/api/v1/admin/domain_blocks/${domainBlockId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(adminDomainBlockSchema, response.json); - }, - - /** - * Remove a domain block - * Lift a block against a domain. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete} - */ - deleteDomainBlock: async (domainBlockId: string) => { - const response = await this.request( - `/api/v1/admin/domain_blocks/${domainBlockId}`, - { - method: 'DELETE', - }, - ); - - return response.json; - }, - }, - - /** Perform moderation actions with reports. */ - reports: { - /** - * View all reports - * View information about all reports. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#get} - */ - getReports: (params?: AdminGetReportsParams) => { - if (this.features.mastodonAdmin) { - if ( - params?.resolved === undefined && - (this.features.version.software === GOTOSOCIAL || - this.features.version.software === PLEROMA) - ) { - if (!params) params = {}; - params.resolved = false; - } - return this.#paginatedGet('/api/v1/admin/reports', { params }, adminReportSchema); - } - - return this.#paginatedPleromaReports({ - state: params?.resolved === true ? 'resolved' : 'open', - page_size: params?.limit || 100, - }); - }, - - /** - * View a single report - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#get-one} - */ - getReport: async (reportId: string) => { - let response; - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/reports/${reportId}`); - } else { - response = await this.request(`/api/v1/pleroma/admin/reports/${reportId}`); - } - - return v.parse(adminReportSchema, response.json); - }, - - /** - * Update a report - * Change metadata for a report. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#update} - */ - updateReport: async (reportId: string, params: AdminUpdateReportParams) => { - const response = await this.request(`/api/v1/admin/reports/${reportId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(adminReportSchema, response.json); - }, - - /** - * Assign report to self - * Claim the handling of this report to yourself. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#assign_to_self} - */ - assignReportToSelf: async (reportId: string) => { - const response = await this.request(`/api/v1/admin/reports/${reportId}/assign_to_self`, { - method: 'POST', - }); - - return v.parse(adminReportSchema, response.json); - }, - - /** - * Unassign report - * Unassign a report so that someone else can claim it. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#unassign} - */ - unassignReport: async (reportId: string) => { - const response = await this.request(`/api/v1/admin/reports/${reportId}/unassign`, { - method: 'POST', - }); - - return v.parse(adminReportSchema, response.json); - }, - - /** - * Mark report as resolved - * - * Mark a report as resolved with no further action taken. - * - * `action_taken_comment` param requires features{@link Features.mastodonAdminResolveReportWithComment}. - * @param action_taken_comment Optional admin comment on the action taken in response to this report. Supported by GoToSocial only. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#resolve} - */ - resolveReport: async (reportId: string, action_taken_comment?: string) => { - let response; - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/reports/${reportId}/resolve`, { - method: 'POST', - body: { action_taken_comment }, - }); - } else { - response = await this.request(`/api/v1/pleroma/admin/reports/${reportId}`, { - method: 'PATCH', - body: { reports: [{ id: reportId, state: 'resolved' }] }, - }); - } - - return v.parse(adminReportSchema, response.json); - }, - - /** - * Reopen a closed report - * Reopen a currently closed report, if it is closed. - * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#reopen} - */ - reopenReport: async (reportId: string) => { - let response; - if (this.features.mastodonAdmin) { - response = await this.request(`/api/v1/admin/reports/${reportId}/reopen`, { - method: 'POST', - }); - } else { - response = await this.request(`/api/v1/pleroma/admin/reports/${reportId}`, { - method: 'PATCH', - body: { reports: [{ id: reportId, state: 'open' }] }, - }); - } - - return v.parse(adminReportSchema, response.json); - }, - }, - - statuses: { - /** - * @param params Retrieves all latest statuses - * - * The params are subject to change in case Mastodon implements alike route. - * - * Requires features{@link Features.pleromaAdminStatuses}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminstatuses} - */ - getStatuses: (params?: AdminGetStatusesParams) => - this.#paginatedPleromaStatuses({ - page_size: params?.limit || 100, - page: 1, - local_only: params?.local_only, - with_reblogs: params?.with_reblogs, - godmode: params?.with_private, - }), - - /** - * Show status by id - * - * Requires features{@link Features.pleromaAdminStatuses}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminstatusesid} - */ - getStatus: async (statusId: string) => { - const response = await this.request(`/api/v1/pleroma/admin/statuses/${statusId}`); - - return v.parse(statusSchema, response.json); - }, - - /** - * Change the scope of an individual reported status - * - * Requires features{@link Features.pleromaAdminStatuses}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#put-apiv1pleromaadminstatusesid} - */ - updateStatus: async (statusId: string, params: AdminUpdateStatusParams) => { - const response = await this.request(`/api/v1/pleroma/admin/statuses/${statusId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Delete an individual reported status - * - * Requires features{@link Features.pleromaAdminStatuses}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminstatusesid} - */ - deleteStatus: async (statusId: string) => { - let response; - - if (this.features.version.software === MITRA) { - response = await this.request(`/api/v1/admin/posts/${statusId}`, { - method: 'DELETE', - }); - } else { - response = await this.request(`/api/v1/pleroma/admin/statuses/${statusId}`, { - method: 'DELETE', - }); - } - - return response.json; - }, - - /** - * Requires features{@link Features.pleromaAdminStatusesRedact} - */ - redactStatus: async ( - statusId: string, - params: EditStatusParams & { overwrite?: boolean }, - ) => { - const response = await this.request(`/api/v1/pleroma/admin/statuses/${statusId}/redact`, { - method: 'PATCH', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Requires features{@link Features.pleromaAdminStatusesRedact} - */ - getStatusSource: async (statusId: string) => { - const response = await this.request(`/api/v1/pleroma/admin/statuses/${statusId}/source`); - - return v.parse(statusSourceSchema, response.json); - }, - }, - - trends: { - /** - * View trending links - * Links that have been shared more than others, including unapproved and unreviewed links. - * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#links} - */ - getTrendingLinks: async () => { - const response = await this.request('/api/v1/admin/trends/links'); - - return v.parse(filteredArray(trendsLinkSchema), response.json); - }, - - /** - * View trending statuses - * Statuses that have been interacted with more than others, including unapproved and unreviewed statuses. - * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#statuses} - */ - getTrendingStatuses: async () => { - const response = await this.request('/api/v1/admin/trends/statuses'); - - return v.parse(filteredArray(statusSchema), response.json); - }, - - /** - * View trending tags - * Tags that are being used more frequently within the past week, including unapproved and unreviewed tags. - * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#tags} - */ - getTrendingTags: async () => { - const response = await this.request('/api/v1/admin/trends/links'); - - return v.parse(filteredArray(adminTagSchema), response.json); - }, - }, - - /** Block certain email addresses by their hash. */ - canonicalEmailBlocks: { - /** - * List all canonical email blocks - * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#get} - */ - getCanonicalEmailBlocks: (params?: AdminGetCanonicalEmailBlocks) => - this.#paginatedGet( - '/api/v1/admin/canonical_email_blocks', - { params }, - adminCanonicalEmailBlockSchema, - ), - - /** - * Show a single canonical email block - * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#get-one} - */ - getCanonicalEmailBlock: async (canonicalEmailBlockId: string) => { - const response = await this.request( - `/api/v1/admin/canonical_email_blocks/${canonicalEmailBlockId}`, - ); - - return v.parse(adminCanonicalEmailBlockSchema, response.json); - }, - - /** - * Test - * Canoniocalize and hash an email address. - * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#test} - */ - testCanonicalEmailBlock: async (email: string) => { - const response = await this.request('/api/v1/admin/canonical_email_blocks/test', { - method: 'POST', - body: { email }, - }); - - return v.parse(filteredArray(adminCanonicalEmailBlockSchema), response.json); - }, - - /** - * Block a canonical email - * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#create} - */ - createCanonicalEmailBlock: async (email: string, canonical_email_hash?: string) => { - const response = await this.request('/api/v1/admin/canonical_email_blocks', { - method: 'POST', - body: { email, canonical_email_hash }, - }); - - return v.parse(filteredArray(adminCanonicalEmailBlockSchema), response.json); - }, - - /** - * Delete a canonical email block - * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#delete} - */ - deleteCanonicalEmailBlock: async (canonicalEmailBlockId: string) => { - const response = await this.request( - `/api/v1/admin/canonical_email_blocks/${canonicalEmailBlockId}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - }, - - /** Obtain qualitative metrics about the server. */ - dimensions: { - /** - * Get dimensional data - * Obtain information about popularity of certain accounts, servers, languages, etc. - * @see {@link https://docs.joinmastodon.org/methods/admin/dimensions/#get} - */ - getDimensions: async (keys: AdminDimensionKey[], params?: AdminGetDimensionsParams) => { - const response = await this.request('/api/v1/admin/dimensions', { - method: 'POST', - params: { ...params, keys }, - }); - - return v.parse(filteredArray(adminDimensionSchema), response.json); - }, - }, - - /** Allow certain domains to federate. */ - domainAllows: { - /** - * List all allowed domains - * Show information about all allowed domains. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#get} - */ - getDomainAllows: (params?: AdminGetDomainAllowsParams) => - this.#paginatedGet('/api/v1/admin/domain_allows', { params }, adminDomainAllowSchema), - - /** - * Get a single allowed domain - * Show information about a single allowed domain. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#get-one} - */ - getDomainAllow: async (domainAllowId: string) => { - const response = await this.request(`/api/v1/admin/domain_allows/${domainAllowId}`); - - return v.parse(adminDomainAllowSchema, response.json); - }, - - /** - * Allow a domain to federate - * Add a domain to the list of domains allowed to federate, to be used when the instance is in allow-list federation mode. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#create} - */ - createDomainAllow: async (domain: string) => { - const response = await this.request('/api/v1/admin/domain_allows', { - method: 'POST', - body: { domain }, - }); - - return v.parse(adminDomainAllowSchema, response.json); - }, - - /** - * Delete an allowed domain - * Delete a domain from the allowed domains list. - * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#delete} - */ - deleteDomainAllow: async (domainAllowId: string) => { - const response = await this.request( - `/api/v1/admin/domain_allows/${domainAllowId}`, - { - method: 'DELETE', - }, - ); - - return response.json; - }, - }, - - /** Disallow certain email domains from signing up. */ - emailDomainBlocks: { - /** - * List all blocked email domains - * Show information about all email domains blocked from signing up. - * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#get} - */ - getEmailDomainBlocks: (params?: AdminGetEmailDomainBlocksParams) => - this.#paginatedGet( - '/api/v1/admin/email_domain_blocks', - { params }, - adminEmailDomainBlockSchema, - ), - - /** - * Get a single blocked email domain - * Show information about a single email domain that is blocked from signups. - * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#get-one} - */ - getEmailDomainBlock: async (emailDomainBlockId: string) => { - const response = await this.request( - `/api/v1/admin/email_domain_blocks/${emailDomainBlockId}`, - ); - - return v.parse(adminEmailDomainBlockSchema, response.json); - }, - - /** - * Block an email domain from signups - * Add a domain to the list of email domains blocked from signups. - * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#create} - */ - createEmailDomainBlock: async (domain: string) => { - const response = await this.request('/api/v1/admin/email_domain_blocks', { - method: 'POST', - body: { domain }, - }); - - return v.parse(adminEmailDomainBlockSchema, response.json); - }, - - /** - * Delete an email domain block - * Lift a block against an email domain. - * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#delete} - */ - deleteEmailDomainBlock: async (emailDomainBlockId: string) => { - const response = await this.request( - `/api/v1/admin/email_domain_blocks/${emailDomainBlockId}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - }, - - /** Disallow certain IP address ranges from signing up. */ - ipBlocks: { - /** - * List all IP blocks - * Show information about all blocked IP ranges. - * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#get} - */ - getIpBlocks: (params?: AdminGetIpBlocksParams) => - this.#paginatedGet('/api/v1/admin/ip_blocks', { params }, adminIpBlockSchema), - - /** - * Get a single IP block - * Show information about a single IP block. - * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#get-one} - */ - getIpBlock: async (ipBlockId: string) => { - const response = await this.request(`/api/v1/admin/ip_blocks/${ipBlockId}`); - - return v.parse(adminIpBlockSchema, response.json); - }, - - /** - * Block an IP address range from signing up - * Add an IP address range to the list of IP blocks. - * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#create} - */ - createIpBlock: async (params: AdminCreateIpBlockParams) => { - const response = await this.request('/api/v1/admin/ip_blocks', { - method: 'POST', - body: params, - }); - - return v.parse(adminIpBlockSchema, response.json); - }, - - /** - * Update a domain block - * Change parameters for an existing IP block. - * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#update} - */ - updateIpBlock: async (ipBlockId: string, params: AdminCreateIpBlockParams) => { - const response = await this.request(`/api/v1/admin/ip_blocks/${ipBlockId}`, { - method: 'POST', - body: params, - }); - - return v.parse(adminIpBlockSchema, response.json); - }, - - /** - * Delete an IP block - * Lift a block against an IP range. - * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#delete} - */ - deleteIpBlock: async (ipBlockId: string) => { - const response = await this.request(`/api/v1/admin/ip_blocks/${ipBlockId}`, { - method: 'DELETE', - }); - - return response.json; - }, - }, - - /** Obtain quantitative metrics about the server. */ - measures: { - /** - * Get measurable data - * Obtain quantitative metrics about the server. - * @see {@link https://docs.joinmastodon.org/methods/admin/measures/#get} - */ - getMeasures: async ( - keys: AdminMeasureKey[], - start_at: string, - end_at: string, - params?: AdminGetMeasuresParams, - ) => { - const response = await this.request('/api/v1/admin/measures', { - method: 'POST', - params: { ...params, keys, start_at, end_at }, - }); - - return v.parse(filteredArray(adminMeasureSchema), response.json); - }, - }, - - /** Show retention data over time. */ - retention: { - /** - * Calculate retention data - * - * Generate a retention data report for a given time period and bucket. - * @see {@link https://docs.joinmastodon.org/methods/admin/retention/#create} - */ - getRetention: async (start_at: string, end_at: string, frequency: 'day' | 'month') => { - const response = await this.request('/api/v1/admin/retention', { - method: 'POST', - params: { start_at, end_at, frequency }, - }); - - return v.parse(filteredArray(adminCohortSchema), response.json); - }, - }, - - announcements: { - /** - * List announcements - * - * Requires features{@link Features.pleromaAdminAnnouncements}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} - */ - getAnnouncements: async ( - params?: AdminGetAnnouncementsParams, - ): Promise> => { - const response = await this.request('/api/v1/pleroma/admin/announcements', { params }); - - const items = v.parse(filteredArray(adminAnnouncementSchema), response.json); - - return { - previous: null, - next: items.length - ? () => - this.admin.announcements.getAnnouncements({ - ...params, - offset: (params?.offset || 0) + items.length, - }) - : null, - items, - partial: false, - }; - }, - - /** - * Display one announcement - * - * Requires features{@link Features.pleromaAdminAnnouncements}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncementsid} - */ - getAnnouncement: async (announcementId: string) => { - const response = await this.request( - `/api/v1/pleroma/admin/announcements/${announcementId}`, - ); - - return v.parse(adminAnnouncementSchema, response.json); - }, - - /** - * Create an announcement - * - * Requires features{@link Features.pleromaAdminAnnouncements}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminannouncements} - */ - createAnnouncement: async (params: AdminCreateAnnouncementParams) => { - const response = await this.request('/api/v1/pleroma/admin/announcements', { - method: 'POST', - body: params, - }); - - return v.parse(adminAnnouncementSchema, response.json); - }, - - /** - * Change an announcement - * - * Requires features{@link Features.pleromaAdminAnnouncements}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminannouncementsid} - */ - updateAnnouncement: async (announcementId: string, params: AdminUpdateAnnouncementParams) => { - const response = await this.request( - `/api/v1/pleroma/admin/announcements/${announcementId}`, - { method: 'PATCH', body: params }, - ); - - return v.parse(adminAnnouncementSchema, response.json); - }, - - /** - * Delete an announcement - * - * Requires features{@link Features.pleromaAdminAnnouncements}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminannouncementsid} - */ - deleteAnnouncement: async (announcementId: string) => { - const response = await this.request( - `/api/v1/pleroma/admin/announcements/${announcementId}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - }, - - domains: { - /** - * List of domains - * - * Requires features{@link Features.domains}. - */ - getDomains: async () => { - const response = await this.request('/api/v1/pleroma/admin/domains'); - - return v.parse(filteredArray(adminDomainSchema), response.json); - }, - - /** - * Create a domain - * - * Requires features{@link Features.domains}. - */ - createDomain: async (params: AdminCreateDomainParams) => { - const response = await this.request('/api/v1/pleroma/admin/domains', { - method: 'POST', - body: params, - }); - - return v.parse(adminDomainSchema, response.json); - }, - - /** - * Change domain publicity - * - * Requires features{@link Features.domains}. - */ - updateDomain: async (domainId: string, isPublic: boolean) => { - const response = await this.request(`/api/v1/pleroma/admin/domains/${domainId}`, { - method: 'PATCH', - body: { public: isPublic }, - }); - - return v.parse(adminDomainSchema, response.json); - }, - - /** - * Delete a domain - * - * Requires features{@link Features.domains}. - */ - deleteDomain: async (domainId: string) => { - const response = await this.request( - `/api/v1/pleroma/admin/domains/${domainId}`, - { - method: 'DELETE', - }, - ); - - return response.json; - }, - }, - - moderationLog: { - /** - * Get moderation log - * - * Requires features{@link Features.pleromaAdminModerationLog}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminmoderation_log} - */ - getModerationLog: async ({ limit, ...params }: AdminGetModerationLogParams = {}): Promise< - PaginatedResponse - > => { - const response = await this.request('/api/v1/pleroma/admin/moderation_log', { - params: { page_size: limit, ...params }, - }); - - const items = v.parse(filteredArray(adminModerationLogEntrySchema), response.json.items); - - return { - previous: - params.page && params.page > 1 - ? () => - this.admin.moderationLog.getModerationLog({ ...params, page: params.page! - 1 }) - : null, - next: - response.json.total > (params.page || 1) * (limit || 50) - ? () => - this.admin.moderationLog.getModerationLog({ - ...params, - page: (params.page || 1) + 1, - }) - : null, - items, - partial: response.status === 206, - }; - }, - }, - - relays: { - /** - * List Relays - * - * Requires features{@link Features.pleromaAdminRelays}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrelay} - */ - getRelays: async () => { - const response = await this.request('/api/v1/pleroma/admin/relay'); - - return v.parse(filteredArray(adminRelaySchema), response.json); - }, - - /** - * Follow a Relay - * - * Requires features{@link Features.pleromaAdminRelays}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminrelay} - */ - followRelay: async (relayUrl: string) => { - const response = await this.request('/api/v1/pleroma/admin/relay', { - method: 'POST', - body: { relay_url: relayUrl }, - }); - - return v.parse(adminRelaySchema, response.json); - }, - - /** - * Unfollow a Relay - * - * Requires features{@link Features.pleromaAdminRelays}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminrelay} - */ - unfollowRelay: async (relayUrl: string, force = false) => { - const response = await this.request('/api/v1/pleroma/admin/relay', { - method: 'DELETE', - body: { relay_url: relayUrl, force }, - }); - - return v.parse(adminRelaySchema, response.json); - }, - }, - - rules: { - /** - * List rules - * - * Requires features{@link Features.adminRules}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrules} - */ - getRules: async () => { - const response = await this.request( - this.features.version.software === GOTOSOCIAL - ? '/api/v1/admin/instance/rules' - : '/api/v1/pleroma/admin/rules', - ); - - return v.parse(filteredArray(adminRuleSchema), response.json); - }, - - /** - * Create a rule - * - * Requires features{@link Features.adminRules}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminrules} - */ - createRule: async (params: AdminCreateRuleParams) => { - const response = await this.request( - this.features.version.software === GOTOSOCIAL - ? '/api/v1/admin/instance/rules' - : '/api/v1/pleroma/admin/rules', - { method: 'POST', body: params }, - ); - - return v.parse(adminRuleSchema, response.json); - }, - - /** - * Update a rule - * - * Requires features{@link Features.adminRules}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminrulesid} - */ - updateRule: async (ruleId: string, params: AdminUpdateRuleParams) => { - const response = await this.request( - `/api/v1/${this.features.version.software === GOTOSOCIAL ? 'admin/instance' : 'pleroma/admin'}/rules/${ruleId}`, - { method: 'PATCH', body: params }, - ); - - return v.parse(adminRuleSchema, response.json); - }, - - /** - * Delete a rule - * - * Requires features{@link Features.adminRules}. - * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminrulesid} - */ - deleteRule: async (ruleId: string) => { - const response = await this.request( - `/api/v1/${this.features.version.software === GOTOSOCIAL ? 'admin/instance' : 'pleroma/admin'}/rules/${ruleId}`, - { method: 'DELETE' }, - ); - - return response.json; - }, - }, - - config: { - getPleromaConfig: async () => { - const response = await this.request('/api/v1/pleroma/admin/config'); - - return v.parse(pleromaConfigSchema, response.json); - }, - - updatePleromaConfig: async (params: PleromaConfig['configs']) => { - const response = await this.request('/api/v1/pleroma/admin/config', { - method: 'POST', - body: { configs: params }, - }); - - return v.parse(pleromaConfigSchema, response.json); - }, - }, - - customEmojis: { - /** - * View local and remote emojis available to/known by this instance. - * - * Requires features{@link Features.adminCustomEmojis}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - getCustomEmojis: (params: AdminGetCustomEmojisParams) => - this.#paginatedGet('/api/v1/admin/custom_emojis', { params }, adminCustomEmojiSchema), - - /** - * Get the admin view of a single emoji. - * - * Requires features{@link Features.adminCustomEmojis}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - getCustomEmoji: async (emojiId: string) => { - const response = await this.request(`/api/v1/admin/custom_emojis/${emojiId}`); - - return v.parse(adminCustomEmojiSchema, response.json); - }, - - /** - * Get the admin view of a single emoji. - * - * Requires features{@link Features.adminCustomEmojis}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - createCustomEmoji: async (params: AdminCreateCustomEmojiParams) => { - const response = await this.request('/api/v1/admin/custom_emojis', { - method: 'POST', - body: params, - contentType: '', - }); - - return v.parse(adminCustomEmojiSchema, response.json); - }, - - updateCustomEmoji: async (emojiId: string, params: AdminUpdateCustomEmojiParams) => { - const response = await this.request(`/api/v1/admin/custom_emojis/${emojiId}`, { - method: 'PATCH', - body: params, - contentType: '', - }); - - return v.parse(adminCustomEmojiSchema, response.json); - }, - - /** - * Delete a **local** emoji with the given ID from the instance. - * - * Requires features{@link Features.adminCustomEmojis}. - * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} - */ - deleteCustomEmoji: async (emojiId: string) => { - const response = await this.request(`/api/v1/admin/custom_emojis/${emojiId}`, { - method: 'DELETE', - }); - - return v.parse(adminCustomEmojiSchema, response.json); - }, - }, - }; - - public readonly oembed = { - /** - * Get OEmbed info as JSON - * @see {@link https://docs.joinmastodon.org/methods/oembed/#get} - */ - getOembed: async (url: string, maxwidth?: number, maxheight?: number) => { - const response = await this.request('/api/oembed', { params: { url, maxwidth, maxheight } }); - - return v.parse( - v.object({ - type: v.fallback(v.string(), 'rich'), - version: v.fallback(v.string(), ''), - author_name: v.fallback(v.string(), ''), - author_url: v.fallback(v.string(), ''), - provider_name: v.fallback(v.string(), ''), - provider_url: v.fallback(v.string(), ''), - cache_age: v.number(), - html: v.string(), - width: v.fallback(v.nullable(v.number()), null), - height: v.fallback(v.nullable(v.number()), null), - }), - response.json, - ); - }, - }; - - /** @see {@link https://docs.pleroma.social/backend/development/API/chats} */ - public readonly chats = { - /** - * create or get an existing Chat for a certain recipient - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#creating-or-getting-a-chat} - */ - createChat: async (accountId: string) => { - const response = await this.request(`/api/v1/pleroma/chats/by-account-id/${accountId}`, { - method: 'POST', - }); - - return v.parse(chatSchema, response.json); - }, - - /** - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#creating-or-getting-a-chat} - */ - getChat: async (chatId: string) => { - const response = await this.request(`/api/v1/pleroma/chats/${chatId}`); - - return v.parse(chatSchema, response.json); - }, - - /** - * Marking a chat as read - * mark a number of messages in a chat up to a certain message as read - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#marking-a-chat-as-read} - */ - markChatAsRead: async (chatId: string, last_read_id: string) => { - const response = await this.request(`/api/v1/pleroma/chats/${chatId}/read`, { - method: 'POST', - body: { last_read_id }, - }); - - return v.parse(chatSchema, response.json); - }, - - /** - * Marking a single chat message as read - * To set the `unread` property of a message to `false` - * https://docs.pleroma.social/backend/development/API/chats/#marking-a-single-chat-message-as-read - */ - markChatMessageAsRead: async (chatId: string, chatMessageId: string) => { - const response = await this.request( - `/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}/read`, - { method: 'POST' }, - ); - - return v.parse(chatSchema, response.json); - }, - - /** - * Getting a list of Chats - * This will return a list of chats that you have been involved in, sorted by their last update (so new chats will be at the top). - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-a-list-of-chats} - */ - getChats: (params?: GetChatsParams) => - this.#paginatedGet('/api/v2/pleroma/chats', { params }, chatSchema), - - /** - * Getting the messages for a Chat - * For a given Chat id, you can get the associated messages with - */ - getChatMessages: (chatId: string, params?: GetChatMessagesParams) => - this.#paginatedGet(`/api/v1/pleroma/chats/${chatId}/messages`, { params }, chatMessageSchema), - - /** - * Posting a chat message - * Posting a chat message for given Chat id works like this: - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#posting-a-chat-message} - */ - createChatMessage: async (chatId: string, params: CreateChatMessageParams) => { - const response = await this.request(`/api/v1/pleroma/chats/${chatId}/messages`, { - method: 'POST', - body: params, - }); - - return v.parse(chatMessageSchema, response.json); - }, - - /** - * Deleting a chat message - * Deleting a chat message for given Chat id works like this: - * @see {@link https://docs.pleroma.social/backend/development/API/chats/#deleting-a-chat-message} - */ - deleteChatMessage: async (chatId: string, messageId: string) => { - const response = await this.request(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`, { - method: 'DELETE', - }); - - return v.parse(chatMessageSchema, response.json); - }, - - /** - * Deleting a chat - * - * Requires features{@link Features.chatsDelete}. - */ - deleteChat: async (chatId: string) => { - const response = await this.request(`/api/v1/pleroma/chats/${chatId}`, { method: 'DELETE' }); - - return v.parse(chatSchema, response.json); - }, - }; - - public readonly shoutbox = { - connect: ( - token: string, - { - onMessage, - onMessages, - }: { - onMessages: (messages: Array) => void; - onMessage: (message: ShoutMessage) => void; - }, - ) => { - let counter = 2; - let intervalId: NodeJS.Timeout; - if (this.#shoutSocket) return this.#shoutSocket; - - const path = buildFullPath('/socket/websocket', this.baseURL, { token, vsn: '2.0.0' }); - - const ws = new WebSocket(path); - - ws.onmessage = (event) => { - const [_, __, ___, type, payload] = JSON.parse(event.data as string); - if (type === 'new_msg') { - const message = v.parse(shoutMessageSchema, payload); - onMessage(message); - } else if (type === 'messages') { - const messages = v.parse(filteredArray(shoutMessageSchema), payload.messages); - onMessages(messages); - } - }; - - ws.onopen = () => { - ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'phx_join', {}])); - - intervalId = setInterval(() => { - ws.send(JSON.stringify([null, `${++counter}`, 'phoenix', 'heartbeat', {}])); - }, 5000); - }; - - ws.onclose = () => { - clearInterval(intervalId); - }; - - this.#shoutSocket = { - message: (text: string) => { - // guess this is meant to be incremented on each call but idk - ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'new_msg', { text: text }])); - }, - close: () => { - ws.close(); - this.#shoutSocket = undefined; - clearInterval(intervalId); - }, - }; - - return this.#shoutSocket; - }, - }; - - public readonly events = { - /** - * Creates an event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events} - */ - createEvent: async (params: CreateEventParams) => { - const response = await this.request('/api/v1/pleroma/events', { - method: 'POST', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Edits an event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id} - */ - editEvent: async (statusId: string, params: EditEventParams) => { - const response = await this.request(`/api/v1/pleroma/events/${statusId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Gets user's joined events - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-joined_events} - */ - getJoinedEvents: (state?: 'pending' | 'reject' | 'accept', params?: GetJoinedEventsParams) => - this.#paginatedGet( - '/api/v1/pleroma/events/joined_events', - { params: { ...params, state } }, - statusSchema, - ), - - /** - * Gets event participants - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participations} - */ - getEventParticipations: (statusId: string, params?: GetEventParticipationsParams) => - this.#paginatedGet( - `/api/v1/pleroma/events/${statusId}/participations`, - { params }, - accountSchema, - ), - - /** - * Gets event participation requests - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests} - */ - getEventParticipationRequests: ( - statusId: string, - params?: GetEventParticipationRequestsParams, - ) => - this.#paginatedGet( - `/api/v1/pleroma/events/${statusId}/participation_requests`, - { params }, - v.object({ - account: accountSchema, - participation_message: v.fallback(v.string(), ''), - }), - ), - - /** - * Accepts user to the event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests-participant_id-authorize} - */ - acceptEventParticipationRequest: async (statusId: string, accountId: string) => { - const response = await this.request( - `/api/v1/pleroma/events/${statusId}/participation_requests/${accountId}/authorize`, - { method: 'POST' }, - ); - - return v.parse(statusSchema, response.json); - }, - - /** - * Rejects user from the event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests-participant_id-reject} - */ - rejectEventParticipationRequest: async (statusId: string, accountId: string) => { - const response = await this.request( - `/api/v1/pleroma/events/${statusId}/participation_requests/${accountId}/reject`, - { method: 'POST' }, - ); - - return v.parse(statusSchema, response.json); - }, - - /** - * Joins the event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-join} - */ - joinEvent: async (statusId: string, participation_message?: string) => { - const response = await this.request(`/api/v1/pleroma/events/${statusId}/join`, { - method: 'POST', - body: { participation_message }, - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Leaves the event - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-leave} - */ - leaveEvent: async (statusId: string) => { - const response = await this.request(`/api/v1/pleroma/events/${statusId}/leave`, { - method: 'POST', - }); - - return v.parse(statusSchema, response.json); - }, - - /** - * Event ICS file - * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#event-ics-file} - */ - getEventIcs: async (statusId: string) => { - const response = await this.request(`/api/v1/pleroma/events/${statusId}/ics`, { - contentType: '', - }); - - return response.data; - }, - }; - - public readonly interactionRequests = { - /** - * Get an array of interactions requested on your statuses by other accounts, and pending your approval. - * - * Requires features{@link Features.interactionRequests}. - */ - getInteractionRequests: (params?: GetInteractionRequestsParams) => - this.#paginatedGet('/api/v1/interaction_requests', { params }, interactionRequestSchema), - - /** - * Get interaction request with the given ID. - * - * Requires features{@link Features.interactionRequests}. - */ - getInteractionRequest: async (interactionRequestId: string) => { - const response = await this.request(`/api/v1/interaction_requests/${interactionRequestId}`); - - return v.parse(interactionRequestSchema, response.json); - }, - - /** - * Accept/authorize/approve an interaction request with the given ID. - * - * Requires features{@link Features.interactionRequests}. - */ - authorizeInteractionRequest: async (interactionRequestId: string) => { - const response = await this.request( - `/api/v1/interaction_requests/${interactionRequestId}/authorize`, - { method: 'POST' }, - ); - - return v.parse(interactionRequestSchema, response.json); - }, - - /** - * Reject an interaction request with the given ID. - * - * Requires features{@link Features.interactionRequests}. - */ - rejectInteractionRequest: async (interactionRequestId: string) => { - const response = await this.request( - `/api/v1/interaction_requests/${interactionRequestId}/authorize`, - { method: 'POST' }, - ); - - return v.parse(interactionRequestSchema, response.json); - }, - }; - - public readonly antennas = { - /** - * Requires features{@link Features.antennas}. - */ - fetchAntennas: async () => { - const response = await this.request('/api/v1/antennas'); - - return v.parse(filteredArray(antennaSchema), response.json); - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennas: async (antennaId: string) => { - const response = await this.request(`/api/v1/antennas/${antennaId}`); - - return v.parse(antennaSchema, response.json); - }, - - /** - * Requires features{@link Features.antennas}. - */ - createAntenna: async (params: CreateAntennaParams) => { - const response = await this.request('/api/v1/antennas', { method: 'POST', body: params }); - - return v.parse(antennaSchema, response.json); - }, - - /** - * Requires features{@link Features.antennas}. - */ - updateAntenna: async (antennaId: string, params: UpdateAntennaParams) => { - const response = await this.request(`/api/v1/antennas/${antennaId}`, { - method: 'PUT', - body: params, - }); - - return v.parse(antennaSchema, response.json); - }, - - /** - * Requires features{@link Features.antennas}. - */ - deleteAntenna: async (antennaId: string) => { - const response = await this.request(`/api/v1/antennas/${antennaId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennaAccounts: (antennaId: string) => - this.#paginatedGet(`/api/v1/antennas/${antennaId}/accounts`, {}, accountSchema), - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaAccounts: async (antennaId: string, accountIds: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/accounts`, { - method: 'POST', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaAccounts: async (antennaId: string, accountIds: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/accounts`, { - method: 'DELETE', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennaExcludedAccounts: (antennaId: string) => - this.#paginatedGet(`/api/v1/antennas/${antennaId}/exclude_accounts`, {}, accountSchema), - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaExcludedAccounts: async (antennaId: string, accountIds: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_accounts`, - { - method: 'POST', - body: { account_ids: accountIds }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaExcludedAccounts: async (antennaId: string, accountIds: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_accounts`, - { - method: 'DELETE', - body: { account_ids: accountIds }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennaDomains: async (antennaId: string) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/domains`); - - return v.parse( - v.object({ - domains: filteredArray(v.string()), - exclude_domains: filteredArray(v.string()), - }), - response.json, - ); - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaDomains: async (antennaId: string, domains: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/domains`, { - method: 'POST', - body: { domains }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaDomains: async (antennaId: string, domains: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/domains`, { - method: 'DELETE', - body: { domains }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaExcludedDomains: async (antennaId: string, domains: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_domains`, - { - method: 'POST', - body: { domains }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaExcludedDomains: async (antennaId: string, domains: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_domains`, - { - method: 'DELETE', - body: { domains }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennaKeywords: async (antennaId: string) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/keywords`); - - return v.parse( - v.object({ - keywords: filteredArray(v.string()), - exclude_keywords: filteredArray(v.string()), - }), - response.json, - ); - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaKeywords: async (antennaId: string, keywords: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/keywords`, { - method: 'POST', - body: { keywords }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaKeywords: async (antennaId: string, keywords: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/keywords`, { - method: 'DELETE', - body: { keywords }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaExcludedKeywords: async (antennaId: string, keywords: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_keywords`, - { - method: 'POST', - body: { keywords }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaExcludedKeywords: async (antennaId: string, keywords: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_keywords`, - { - method: 'DELETE', - body: { keywords }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - getAntennaTags: async (antennaId: string) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/tags`); - - return v.parse( - v.object({ - tags: filteredArray(v.string()), - exclude_tags: filteredArray(v.string()), - }), - response.json, - ); - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaTags: async (antennaId: string, tags: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/tags`, { - method: 'POST', - body: { tags }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaTags: async (antennaId: string, tags: Array) => { - const response = await this.request(`/api/v1/antennas/${antennaId}/tags`, { - method: 'DELETE', - body: { tags }, - }); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - addAntennaExcludedTags: async (antennaId: string, tags: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_tags`, - { - method: 'POST', - body: { tags }, - }, - ); - - return response.json; - }, - - /** - * Requires features{@link Features.antennas}. - */ - removeAntennaExcludedTags: async (antennaId: string, tags: Array) => { - const response = await this.request( - `/api/v1/antennas/${antennaId}/exclude_tags`, - { - method: 'DELETE', - body: { tags }, - }, - ); - - return response.json; - }, - }; - - public readonly circles = { - /** - * Requires features{@link Features.circles}. - */ - fetchCircles: async () => { - const response = await this.request('/api/v1/circles'); - - return v.parse(filteredArray(circleSchema), response.json); - }, - - /** - * Requires features{@link Features.circles}. - */ - getCircle: async (circleId: string) => { - const response = await this.request(`/api/v1/circles/${circleId}`); - - return v.parse(circleSchema, response.json); - }, - - /** - * Requires features{@link Features.circles}. - */ - createCircle: async (title: string) => { - const response = await this.request('/api/v1/circles', { method: 'POST', body: { title } }); - - return v.parse(circleSchema, response.json); - }, - - /** - * Requires features{@link Features.circles}. - */ - updateCircle: async (circleId: string, title: string) => { - const response = await this.request(`/api/v1/circles/${circleId}`, { - method: 'PUT', - body: { title }, - }); - - return v.parse(circleSchema, response.json); - }, - - /** - * Requires features{@link Features.circles}. - */ - deleteCircle: async (circleId: string) => { - const response = await this.request(`/api/v1/circles/${circleId}`, { - method: 'DELETE', - }); - - return response.json; - }, - - /** - * View accounts in a circle - * Requires features{@link Features.circles}. - */ - getCircleAccounts: (circleId: string, params?: GetCircleAccountsParams) => - this.#paginatedGet(`/api/v1/circles/${circleId}/accounts`, { params }, accountSchema), - - /** - * Add accounts to a circle - * Add accounts to the given circle. Note that the user must be following these accounts. - * Requires features{@link Features.circles}. - */ - addCircleAccounts: async (circleId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/circles/${circleId}/accounts`, { - method: 'POST', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** - * Remove accounts from circle - * Remove accounts from the given circle. - * Requires features{@link Features.circles}. - */ - deleteCircleAccounts: async (circleId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/circles/${circleId}/accounts`, { - method: 'DELETE', - body: { account_ids: accountIds }, - }); - - return response.json; - }, - - getCircleStatuses: (circleId: string, params: GetCircleStatusesParams) => - this.#paginatedGet(`/api/v1/circles/${circleId}/statuses`, { params }, statusSchema), - }; - - public readonly rssFeedSubscriptions = { - /** - * Requires features{@link Features.rssFeedSubscriptions}. - */ - fetchRssFeedSubscriptions: async () => { - const response = await this.request('/api/v1/pleroma/rss_feed_subscriptions'); - - return v.parse(filteredArray(rssFeedSchema), response.json); - }, - - /** - * Requires features{@link Features.rssFeedSubscriptions}. - */ - createRssFeedSubscription: async (url: string) => { - const response = await this.request('/api/v1/pleroma/rss_feed_subscriptions', { - method: 'POST', - body: { url }, - }); - - return v.parse(rssFeedSchema, response.json); - }, - - /** - * Requires features{@link Features.rssFeedSubscriptions}. - */ - deleteRssFeedSubscription: async (url: string) => { - const response = await this.request('/api/v1/pleroma/rss_feed_subscriptions', { - method: 'DELETE', - body: { url }, - }); - - return response.json; - }, - }; - - public readonly subscriptions = { - /** - * Add subscriber or extend existing subscription. Can be used if blockchain integration is not enabled. - * - * Requires features{@link Features.subscriptions}. - * @param subscriberId - The subscriber ID. - * @param duration - The subscription duration (in seconds). - */ - createSubscription: async (subscriberId: string, duration: number) => { - const response = await this.request('/api/v1/subscriptions', { - method: 'POST', - body: { subscriber_id: subscriberId, duration }, - }); - - return v.parse(subscriptionDetailsSchema, response.json); - }, - - /** - * Get list of subscription options - * - * Requires features{@link Features.subscriptions}. - */ - getSubscriptionOptions: async () => { - const response = await this.request('/api/v1/subscriptions/options'); - - return v.parse(filteredArray(subscriptionOptionSchema), response.json); - }, - - /** - * Enable subscriptions or update subscription settings - * - * Requires features{@link Features.subscriptions}. - * @param type - Subscription type - * @param chainId - CAIP-2 chain ID. - * @param price - Subscription price (only for Monero) - * @param payoutAddress - Payout address (only for Monero) - */ - updateSubscription: async ( - type: 'monero', - chainId?: string, - price?: number, - payoutAddress?: string, - ) => { - const response = await this.request('/api/v1/subscriptions/options', { - method: 'POST', - body: { type, chain_id: chainId, price, payout_address: payoutAddress }, - }); - - return v.parse(accountSchema, response.json); - }, - - /** - * Find subscription by sender and recipient - * - * Requires features{@link Features.subscriptions}. - * @param senderId - Sender ID. - * @param recipientId - Recipient ID. - */ - findSubscription: async (senderId: string, recipientId: string) => { - const response = await this.request('/api/v1/subscriptions/find', { - params: { sender_id: senderId, recipient_id: recipientId }, - }); - - return v.parse(subscriptionDetailsSchema, response.json); - }, - - /** - * Create invoice - * - * Requires features{@link Features.subscriptions}. - * @param senderId - Sender ID. - * @param recipientId - Recipient ID. - * @param chainId - CAIP-2 chain ID. - * @param amount - Requested payment amount (in atomic units). - */ - createInvoice: async ( - senderId: string, - recipientId: string, - chainId: string, - amount: number, - ) => { - const response = await this.request('/api/v1/subscriptions/invoices', { - method: 'POST', - body: { - sender_id: senderId, - recipient_id: recipientId, - chain_id: chainId, - amount, - }, - }); - - return v.parse(subscriptionInvoiceSchema, response.json); - }, - - /** - * View information about an invoice. - * - * Requires features{@link Features.invoices}. - * @param invoiceId - Invoice ID - */ - getInvoice: async (invoiceId: string) => { - const response = await this.request(`/api/v1/subscriptions/invoices/${invoiceId}`); - - return v.parse(subscriptionInvoiceSchema, response.json); - }, - - /** - * Cancel invoice. - * - * Requires features{@link Features.invoices}. - * @param invoiceId - Invoice ID - */ - cancelInvoice: async (invoiceId: string) => { - const response = await this.request(`/api/v1/subscriptions/invoices/${invoiceId}`, { - method: 'DELETE', - }); - - return v.parse(subscriptionInvoiceSchema, response.json); - }, - }; - - public readonly drive = { - getDrive: async () => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request('/api/iceshrimp/drive/folder'); - - return v.parse(driveFolderSchema, response.json); - }, - - getFolder: async (id: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/folder/${id}`); - - return v.parse(driveFolderSchema, response.json); - }, - - createFolder: async (name: string, parentId?: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request('/api/iceshrimp/drive/folder', { - method: 'POST', - body: { name, parentId: parentId || null }, - }); - - return v.parse(driveFolderSchema, response.json); - }, - - updateFolder: async (id: string, name: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/folder/${id}`, { - method: 'PUT', - body: name, - }); - - return v.parse(driveFolderSchema, response.json); - }, - - deleteFolder: async (id: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/folder/${id}`, { - method: 'DELETE', - }); - - return response; - }, - - moveFolder: async (id: string, targetFolderId?: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/folder/${id}/move`, { - method: 'POST', - body: { folderId: targetFolderId || null }, - }); - - return v.parse(driveFolderSchema, response.json); - }, - - getFile: async (id: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/${id}`); - - return v.parse(driveFileSchema, response.json); - }, - - createFile: async (file: File, folderId?: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request('/api/iceshrimp/drive', { - method: 'POST', - body: { file }, - params: { folderId }, - contentType: '', - }); - - return v.parse(driveFileSchema, response.json); - }, - - updateFile: async (id: string, params: UpdateFileParams) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/${id}`, { - method: 'PATCH', - body: params, - }); - - return v.parse(driveFileSchema, response.json); - }, - - deleteFile: async (id: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request>(`/api/iceshrimp/drive/${id}`, { - method: 'DELETE', - }); - - return response; - }, - - moveFile: async (id: string, targetFolderId?: string) => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request(`/api/iceshrimp/drive/${id}/move`, { - method: 'POST', - body: { folderId: targetFolderId || null }, - }); - - return v.parse(driveFileSchema, response.json); - }, - - getDriveStatus: async () => { - await this.#getIceshrimpAccessToken(); - - const response = await this.request('/api/iceshrimp/drive/status'); - - return v.parse(driveStatusSchema, response.json); - }, - }; - - public readonly stories = { - getRecentStories: async () => { - const response = await this.request('/api/web/stories/v1/recent'); - - return v.parse(filteredArray(storyCarouselItemSchema), response.json); - }, - - getStoryViewers: async (storyId: string) => { - const response = await this.request('/api/web/stories/v1/viewers', { - params: { sid: storyId }, - }); - - return v.parse(filteredArray(accountSchema), response.json); - }, - - getStoriesForProfile: async (accountId: string) => { - const response = await this.request(`/api/web/stories/v1/profile/${accountId}`); - - return v.parse(filteredArray(storyProfileSchema), response.json); - }, - - storyExists: async (accountId: string) => { - const response = await this.request(`/api/web/stories/v1/exists/${accountId}`); - - return v.parse(v.boolean(), response.json); - }, - - getStoryPollResults: async (storyId: string) => { - const response = await this.request('/api/web/stories/v1/poll/results', { - params: { sid: storyId }, - }); - - return v.parse(v.array(v.number()), response.json); - }, - - markStoryAsViewed: async (storyId: string) => { - const response = await this.request('/api/web/stories/v1/viewed', { - method: 'POST', - body: { id: storyId }, - }); - - return response.json; - }, - - createStoryReaction: async (storyId: string, emoji: string) => { - const response = await this.request('/api/web/stories/v1/react', { - method: 'POST', - body: { sid: storyId, reaction: emoji }, - }); - - return response.json; - }, - - createStoryComment: async (storyId: string, comment: string) => { - const response = await this.request('/api/web/stories/v1/comment', { - method: 'POST', - body: { sid: storyId, caption: comment }, - }); - - return response.json; - }, - - createStoryPoll: async (params: CreateStoryPollParams) => { - const response = await this.request('/api/web/stories/v1/publish/poll', { - method: 'POST', - body: params, - }); - - return response.json; - }, - - storyPollVote: async (storyId: string, choiceId: number) => { - const response = await this.request('/api/web/stories/v1/publish/poll', { - method: 'POST', - body: { sid: storyId, ci: choiceId }, - }); - - return response.json; - }, - - reportStory: async (storyId: string, type: StoryReportType) => { - const response = await this.request('/api/web/stories/v1/report', { - method: 'POST', - body: { id: storyId, type }, - }); - - return response.json; - }, - - addMedia: async (file: File) => { - const response = await this.request('/api/web/stories/v1/add', { - method: 'POST', - body: { file }, - contentType: '', - }); - - return v.parse(storyMediaSchema, response.json); - }, - - cropPhoto: async (mediaId: string, params: CropStoryPhotoParams) => { - const response = await this.request('/api/web/stories/v1/crop', { - method: 'POST', - body: { media_id: mediaId, ...params }, - }); - - return response.json; - }, - - createStory: async (mediaId: string, params: CreateStoryParams) => { - const response = await this.request('/api/web/stories/v1/publish', { - method: 'POST', - body: { media_id: mediaId, ...params }, - }); - - return response.json; - }, - - deleteStory: async (storyId: string) => { - const response = await this.request(`/api/web/stories/v1/delete/${storyId}`, { - method: 'DELETE', - }); - - return response.json; - }, - }; - - /** Routes that are not part of any stable release */ - public readonly experimental = { - admin: { - /** @see {@link https://github.com/mastodon/mastodon/pull/19059} */ - groups: { - /** list groups known to the instance. Mimics the interface of `/api/v1/admin/accounts` */ - getGroups: async (params?: AdminGetGroupsParams) => { - const response = await this.request('/api/v1/admin/groups', { params }); - - return v.parse(filteredArray(groupSchema), response.json); - }, - - /** return basic group information */ - getGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/admin/groups/${groupId}`); - - return v.parse(groupSchema, response.json); - }, - - /** suspends a group */ - suspendGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/admin/groups/${groupId}/suspend`, { - method: 'POST', - }); - - return v.parse(groupSchema, response.json); - }, - - /** lift a suspension */ - unsuspendGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/admin/groups/${groupId}/unsuspend`, { - method: 'POST', - }); - - return v.parse(groupSchema, response.json); - }, - - /** deletes an already-suspended group */ - deleteGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/admin/groups/${groupId}`, { - method: 'DELETE', - }); - - return v.parse(groupSchema, response.json); - }, - }, - }, - - /** @see {@link https://github.com/mastodon/mastodon/pull/19059} */ - groups: { - /** returns an array of `Group` entities the current user is a member of */ - getGroups: async () => { - let response; - if (this.features.version.software === PIXELFED) { - response = await this.request('/api/v0/groups/self/list'); - } else { - response = await this.request('/api/v1/groups'); - } - - return v.parse(filteredArray(groupSchema), response.json); - }, - - /** create a group with the given attributes (`display_name`, `note`, `avatar` and `header`). Sets the user who made the request as group administrator */ - createGroup: async (params: CreateGroupParams) => { - let response; - - if (this.features.version.software === PIXELFED) { - response = await this.request('/api/v0/groups/create', { - method: 'POST', - body: { - ...params, - name: params.display_name, - description: params.note, - membership: 'public', - }, - contentType: params.avatar || params.header ? '' : undefined, - }); - - if (response.json?.id) { - return this.experimental.groups.getGroup(response.json.id); - } - } else { - response = await this.request('/api/v1/groups', { - method: 'POST', - body: params, - contentType: params.avatar || params.header ? '' : undefined, - }); - } - - return v.parse(groupSchema, response.json); - }, - - /** returns the `Group` entity describing a given group */ - getGroup: async (groupId: string) => { - let response; - - if (this.features.version.software === PIXELFED) { - response = await this.request(`/api/v0/groups/${groupId}`); - } else { - response = await this.request(`/api/v1/groups/${groupId}`); - } - - return v.parse(groupSchema, response.json); - }, - - /** update group attributes (`display_name`, `note`, `avatar` and `header`) */ - updateGroup: async (groupId: string, params: UpdateGroupParams) => { - const response = await this.request(`/api/v1/groups/${groupId}`, { - method: 'PUT', - body: params, - contentType: params.avatar || params.header ? '' : undefined, - }); - - return v.parse(groupSchema, response.json); - }, - - /** irreversibly deletes the group */ - deleteGroup: async (groupId: string) => { - let response; - - if (this.features.version.software === PIXELFED) { - response = await this.request('/api/v0/groups/delete', { - method: 'POST', - params: { gid: groupId }, - }); - } else { - response = await this.request(`/api/v1/groups/${groupId}`, { - method: 'DELETE', - }); - } - - return response.json; - }, - - /** Has an optional role attribute that can be used to filter by role (valid roles are `"admin"`, `"moderator"`, `"user"`). */ - getGroupMemberships: ( - groupId: string, - role?: GroupRole, - params?: GetGroupMembershipsParams, - ) => - this.#paginatedGet( - this.features.version.software === PIXELFED - ? `/api/v0/groups/members/list?gid=${groupId}` - : `/api/v1/groups/${groupId}/memberships`, - { params: { ...params, role } }, - groupMemberSchema, - ), - - /** returns an array of `Account` entities representing pending requests to join a group */ - getGroupMembershipRequests: (groupId: string, params?: GetGroupMembershipRequestsParams) => - this.#paginatedGet( - this.features.version.software === PIXELFED - ? `/api/v0/groups/members/requests?gid=${groupId}` - : `/api/v1/groups/${groupId}/membership_requests`, - { params }, - accountSchema, - ), - - /** accept a pending request to become a group member */ - acceptGroupMembershipRequest: async (groupId: string, accountId: string) => { - const response = await this.request( - `/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`, - { method: 'POST' }, - ); - - return response.json; - }, - - /** reject a pending request to become a group member */ - rejectGroupMembershipRequest: async (groupId: string, accountId: string) => { - const response = await this.request( - `/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`, - { method: 'POST' }, - ); - - return response.json; - }, - - /** delete a group post (actually marks it as `revoked` if it is a local post) */ - deleteGroupStatus: async (groupId: string, statusId: string) => { - const response = await this.request(`/api/v1/groups/${groupId}/statuses/${statusId}`, { - method: 'DELETE', - }); - - return v.parse(statusSchema, response.json); - }, - - /** list accounts blocked from interacting with the group */ - getGroupBlocks: (groupId: string, params?: GetGroupBlocksParams) => - this.#paginatedGet(`/api/v1/groups/${groupId}/blocks`, { params }, accountSchema), - - /** block one or more users. If they were in the group, they are also kicked of it */ - blockGroupUsers: async (groupId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/groups/${groupId}/blocks`, { - method: 'POST', - params: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** block one or more users. If they were in the group, they are also kicked of it */ - unblockGroupUsers: async (groupId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/groups/${groupId}/blocks`, { - method: 'DELETE', - params: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** joins (or request to join) a given group */ - joinGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/groups/${groupId}/join`, { method: 'POST' }); - - return v.parse(groupRelationshipSchema, response.json); - }, - - /** leaves a given group */ - leaveGroup: async (groupId: string) => { - const response = await this.request(`/api/v1/groups/${groupId}/leave`, { method: 'POST' }); - - return v.parse(groupRelationshipSchema, response.json); - }, - - /** kick one or more group members */ - kickGroupUsers: async (groupId: string, accountIds: string[]) => { - const response = await this.request(`/api/v1/groups/${groupId}/kick`, { - method: 'POST', - params: { account_ids: accountIds }, - }); - - return response.json; - }, - - /** promote one or more accounts to role `new_role`. An error is returned if any of those accounts has a higher role than `new_role` already, or if the role is higher than the issuing user's. Valid roles are `admin`, and `moderator` and `user`. */ - promoteGroupUsers: async (groupId: string, accountIds: string[], role: GroupRole) => { - const response = await this.request(`/api/v1/groups/${groupId}/promote`, { - method: 'POST', - params: { account_ids: accountIds, role }, - }); - - return v.parse(filteredArray(groupMemberSchema), response.json); - }, - - /** demote one or more accounts to role `new_role`. Returns an error unless every of the target account has a strictly lower role than the user (you cannot demote someone with the same role as you), or if any target account already has a role lower than `new_role`. Valid roles are `admin`, `moderator` and `user`. */ - demoteGroupUsers: async (groupId: string, accountIds: string[], role: GroupRole) => { - const response = await this.request(`/api/v1/groups/${groupId}/demote`, { - method: 'POST', - params: { account_ids: accountIds, role }, - }); - - return v.parse(filteredArray(groupMemberSchema), response.json); - }, - - getGroupRelationships: async (groupIds: string[]) => { - const response = await this.request('/api/v1/groups/relationships', { - params: { id: groupIds }, - }); - - return v.parse(filteredArray(groupRelationshipSchema), response.json); - }, - }, - }; - - #setInstance = (instance: Instance) => { - this.#instance = instance; - this.features = getFeatures(this.#instance); - }; - - #getIceshrimpAccessToken = async () => { - if (this.#iceshrimpAccessToken) return; + override getIceshrimpAccessToken = async (): Promise => { + if (this.iceshrimpAccessToken) return; if (this.features.version.software === ICESHRIMP_NET) { - this.#iceshrimpAccessToken = await this.settings.authorizeIceshrimp(); + this.setIceshrimpAccessToken(await this.settings.authorizeIceshrimp()); } }; - - public readonly utils = { - paginatedGet: this.#paginatedGet.bind(this), - }; - - get accessToken(): string | undefined { - return this.#accessToken; - } - - set accessToken(accessToken: string | undefined) { - if (this.#accessToken === accessToken) return; - - this.#socket?.close(); - this.#accessToken = accessToken; - - this.#getIceshrimpAccessToken(); - } - - get iceshrimpAccessToken(): string | undefined { - return this.#iceshrimpAccessToken; - } - - get customAuthorizationToken(): string | undefined { - return this.#customAuthorizationToken; - } - - set customAuthorizationToken(token: string | undefined) { - this.#customAuthorizationToken = token; - } - - get instanceInformation() { - return this.#instance; - } } export { PlApiClient as default }; +export { + accounts, + admin, + announcements, + antennas, + apps, + asyncRefreshes, + chats, + circles, + drive, + emails, + events, + experimental, + filtering, + groupedNotifications, + instance, + interactionRequests, + lists, + media, + myAccount, + notifications, + oauth, + oembed, + polls, + pushNotifications, + rssFeedSubscriptions, + scheduledStatuses, + search, + settings, + shoutbox, + statuses, + stories, + streaming, + subscriptions, + timelines, + trends, + utils, +}; diff --git a/packages/pl-api/lib/client/accounts.ts b/packages/pl-api/lib/client/accounts.ts new file mode 100644 index 000000000..3c8b8808f --- /dev/null +++ b/packages/pl-api/lib/client/accounts.ts @@ -0,0 +1,453 @@ +import * as v from 'valibot'; + +import { + accountSchema, + antennaSchema, + circleSchema, + familiarFollowersSchema, + featuredTagSchema, + listSchema, + relationshipSchema, + reportSchema, + scrobbleSchema, + statusSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { ICESHRIMP_NET, PIXELFED, PLEROMA } from '../features'; +import { type RequestMeta } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateScrobbleParams, + FollowAccountParams, + GetAccountEndorsementsParams, + GetAccountFavouritesParams, + GetAccountFollowersParams, + GetAccountFollowingParams, + GetAccountParams, + GetAccountStatusesParams, + GetAccountSubscribersParams, + GetRelationshipsParams, + GetScrobblesParams, + ReportAccountParams, + SearchAccountParams, +} from '../params/accounts'; + +type EmptyObject = Record; + +const accounts = (client: PlApiBaseClient) => ({ + /** + * Get account + * View information about a profile. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#get} + */ + getAccount: async (accountId: string, params?: GetAccountParams) => { + const response = await client.request(`/api/v1/accounts/${accountId}`, { params }); + + return v.parse(accountSchema, response.json); + }, + + /** + * Get multiple accounts + * View information about multiple profiles. + * + * Requires features{@link Features.getAccounts}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#index} + */ + getAccounts: async (accountId: string[]) => { + const response = await client.request('/api/v1/accounts', { params: { id: accountId } }); + + return v.parse(filteredArray(accountSchema), response.json); + }, + + /** + * Get account’s statuses + * Statuses posted to the given account. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#statuses} + */ + getAccountStatuses: (accountId: string, params?: GetAccountStatusesParams) => + client.paginatedGet(`/api/v1/accounts/${accountId}/statuses`, { params }, statusSchema), + + /** + * Get account’s followers + * Accounts which follow the given account, if network is not hidden by the account owner. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#followers} + */ + getAccountFollowers: (accountId: string, params?: GetAccountFollowersParams) => + client.paginatedGet(`/api/v1/accounts/${accountId}/followers`, { params }, accountSchema), + + /** + * Get account’s following + * Accounts which the given account is following, if network is not hidden by the account owner. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#following} + */ + getAccountFollowing: (accountId: string, params?: GetAccountFollowingParams) => + client.paginatedGet(`/api/v1/accounts/${accountId}/following`, { params }, accountSchema), + + /** + * Subscriptions to the given user. + * + * Requires features{@link Features.subscriptions}. + */ + getAccountSubscribers: (accountId: string, params?: GetAccountSubscribersParams) => + client.paginatedGet(`/api/v1/accounts/${accountId}/subscribers`, { params }, accountSchema), + + /** + * Get account’s featured tags + * Tags featured by this account. + * + * Requires features{@link Features.featuredTags}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#featured_tags} + */ + getAccountFeaturedTags: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/featured_tags`); + + return v.parse(filteredArray(featuredTagSchema), response.json); + }, + + /** + * Get lists containing this account + * User lists that you have added this account to. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#lists} + */ + getAccountLists: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/lists`); + + return v.parse(filteredArray(listSchema), response.json); + }, + + /** + * Get antennas containing this account + * User antennas that you have added this account to. + * Requires features{@link Features.antennas}. + */ + getAccountAntennas: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/antennas`); + + return v.parse(filteredArray(antennaSchema), response.json); + }, + + /** + * Get antennas excluding this account + * Requires features{@link Features.antennas}. + */ + getAccountExcludeAntennas: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/exclude_antennas`); + + return v.parse(filteredArray(circleSchema), response.json); + }, + + /** + * Get circles including this account + * User circles that you have added this account to. + * Requires features{@link Features.circles}. + */ + getAccountCircles: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/circles`); + + return v.parse(filteredArray(antennaSchema), response.json); + }, + + /** + * Follow account + * Follow the given account. Can also be used to update whether to show reblogs or enable notifications. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#follow} + */ + followAccount: async (accountId: string, params?: FollowAccountParams) => { + const response = await client.request(`/api/v1/accounts/${accountId}/follow`, { + method: 'POST', + body: params, + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Unfollow account + * Unfollow the given account. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#unfollow} + */ + unfollowAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/unfollow`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Remove account from followers + * Remove the given account from your followers. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#remove_from_followers} + */ + removeAccountFromFollowers: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/remove_from_followers`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Feature account on your profile + * Add the given account to the user’s featured profiles. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#pin} + */ + pinAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/pin`, { method: 'POST' }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Unfeature account from profile + * Remove the given account from the user’s featured profiles. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#unpin} + */ + unpinAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/unpin`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Set private note on profile + * Sets a private note on a user. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#note} + */ + updateAccountNote: async (accountId: string, comment: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/note`, { + method: 'POST', + body: { comment }, + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Check relationships to other accounts + * Find out whether a given account is followed, blocked, muted, etc. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#relationships} + */ + getRelationships: async (accountIds: string[], params?: GetRelationshipsParams) => { + const response = await client.request('/api/v1/accounts/relationships', { + params: { ...params, id: accountIds }, + }); + + return v.parse(filteredArray(relationshipSchema), response.json); + }, + + /** + * Find familiar followers + * Obtain a list of all accounts that follow a given account, filtered for accounts you follow. + * + * Requires features{@link Features.familiarFollowers}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#familiar_followers} + */ + getFamiliarFollowers: async (accountIds: string[]) => { + let response: any; + + if (client.features.version.software === PIXELFED) { + const settledResponse = await Promise.allSettled( + accountIds.map(async (accountId) => { + const accounts = (await client.request(`/api/v1.1/accounts/mutuals/${accountId}`)).json; + + return { + id: accountId, + accounts, + }; + }), + ); + + response = settledResponse.map((result, index) => + result.status === 'fulfilled' + ? result.value + : { + id: accountIds[index], + accounts: [], + }, + ); + } else { + response = ( + await client.request('/api/v1/accounts/familiar_followers', { params: { id: accountIds } }) + ).json; + } + + return v.parse(filteredArray(familiarFollowersSchema), response); + }, + + /** + * Search for matching accounts + * Search for matching accounts by username or display name. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#search} + */ + searchAccounts: async (q: string, params?: SearchAccountParams, meta?: RequestMeta) => { + const response = await client.request('/api/v1/accounts/search', { + ...meta, + params: { ...params, q }, + }); + + return v.parse(filteredArray(accountSchema), response.json); + }, + + /** + * Lookup account ID from Webfinger address + * Quickly lookup a username to see if it is available, skipping WebFinger resolution. + + * Requires features{@link Features.accountLookup}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#lookup} + */ + lookupAccount: async (acct: string, meta?: RequestMeta) => { + const response = await client.request('/api/v1/accounts/lookup', { ...meta, params: { acct } }); + + return v.parse(accountSchema, response.json); + }, + + /** + * File a report + * @see {@link https://docs.joinmastodon.org/methods/reports/#post} + */ + reportAccount: async (accountId: string, params: ReportAccountParams) => { + const response = await client.request('/api/v1/reports', { + method: 'POST', + body: { ...params, account_id: accountId }, + }); + + return v.parse(reportSchema, response.json); + }, + + /** + * Endorsements + * Returns endorsed accounts + * + * Requires features{@link Features.accountEndorsements}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaaccountsidendorsements} + * @see {@link https://docs.joinmastodon.org/methods/accounts/endorsements} + */ + getAccountEndorsements: (accountId: string, params?: GetAccountEndorsementsParams) => + client.paginatedGet( + `/api/v1/${[PLEROMA].includes(client.features.version.software as string) ? 'pleroma/' : ''}accounts/${accountId}/endorsements`, + { params }, + accountSchema, + ), + + /** + * Birthday reminders + * Birthday reminders about users you follow. + * + * Requires features{@link Features.birthdays}. + */ + getBirthdays: async (day: number, month: number) => { + const response = await client.request('/api/v1/pleroma/birthdays', { params: { day, month } }); + + return v.parse(filteredArray(accountSchema), response.json); + }, + + /** + * Returns favorites timeline of any user + * + * Requires features{@link Features.publicFavourites}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaaccountsidfavourites} + */ + getAccountFavourites: (accountId: string, params?: GetAccountFavouritesParams) => + client.paginatedGet( + `/api/v1/pleroma/accounts/${accountId}/favourites`, + { params }, + statusSchema, + ), + + /** + * Interact with profile or status from remote account + * + * Requires features{@link Features.remoteInteractions}. + * @param ap_id - Profile or status ActivityPub ID + * @param profile - Remote profile webfinger + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromaremote_interaction} + */ + remoteInteraction: async (ap_id: string, profile: string) => { + const response = await client.request('/api/v1/pleroma/remote_interaction', { + method: 'POST', + body: { ap_id, profile }, + }); + + if (response.json?.error) throw response.json.error; + + return v.parse( + v.object({ + url: v.string(), + }), + response.json, + ); + }, + + /** + * Bite the given user. + * + * Requires features{@link Features.bites}. + * @see {@link https://github.com/purifetchi/Toki/blob/master/Toki/Controllers/MastodonApi/Bite/BiteController.cs} + */ + biteAccount: async (accountId: string) => { + let response; + switch (client.features.version.software) { + case ICESHRIMP_NET: + response = await client.request('/api/v1/bite', { + method: 'POST', + body: accountId, + }); + break; + default: + response = await client.request('/api/v1/bite', { + method: 'POST', + params: { id: accountId }, + }); + break; + } + + return response.json; + }, + + /** + * Requests a list of current and recent Listen activities for an account + * + * Requires features{@link Features.scrobbles} + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromaaccountsidscrobbles} + */ + getScrobbles: (accountId: string, params?: GetScrobblesParams) => + client.paginatedGet( + `/api/v1/pleroma/accounts/${accountId}/scrobbles`, + { params }, + scrobbleSchema, + ), + + /** + * Creates a new Listen activity for an account + * + * Requires features{@link Features.scrobbles} + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromascrobble} + */ + createScrobble: async (params: CreateScrobbleParams) => { + if (params.external_link) (params as any).externalLink = params.external_link; + + const response = await client.request('/api/v1/pleroma/scrobble', { body: params }); + + return v.parse(scrobbleSchema, response.json); + }, + + /** + * Load latest activities from outbox + * + * Requires features{@link Features.loadActivities} + */ + loadActivities: async (accountId: string) => { + const response = await client.request( + `/api/v1/accounts/${accountId}/load_activities`, + { method: 'POST' }, + ); + + return response.json; + }, +}); + +export { accounts }; diff --git a/packages/pl-api/lib/client/admin.ts b/packages/pl-api/lib/client/admin.ts new file mode 100644 index 000000000..66f101e75 --- /dev/null +++ b/packages/pl-api/lib/client/admin.ts @@ -0,0 +1,1556 @@ +import * as v from 'valibot'; + +import { + adminAccountSchema, + adminAnnouncementSchema, + adminCanonicalEmailBlockSchema, + adminCohortSchema, + adminCustomEmojiSchema, + adminDimensionSchema, + adminDomainAllowSchema, + adminDomainBlockSchema, + adminDomainSchema, + adminEmailDomainBlockSchema, + adminIpBlockSchema, + adminMeasureSchema, + adminModerationLogEntrySchema, + adminRelaySchema, + adminReportSchema, + adminRuleSchema, + adminTagSchema, + pleromaConfigSchema, + statusSchema, + statusSourceSchema, + trendsLinkSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { GOTOSOCIAL, MITRA, PLEROMA } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + AdminAccount, + AdminAnnouncement, + AdminModerationLogEntry, + AdminReport, + PleromaConfig, + Status, +} from '../entities'; +import type { + AdminAccountAction, + AdminCreateAccountParams, + AdminCreateAnnouncementParams, + AdminCreateCustomEmojiParams, + AdminCreateDomainBlockParams, + AdminCreateDomainParams, + AdminCreateIpBlockParams, + AdminCreateRuleParams, + AdminDimensionKey, + AdminGetAccountsParams, + AdminGetAnnouncementsParams, + AdminGetCanonicalEmailBlocks, + AdminGetCustomEmojisParams, + AdminGetDimensionsParams, + AdminGetDomainAllowsParams, + AdminGetDomainBlocksParams, + AdminGetEmailDomainBlocksParams, + AdminGetIpBlocksParams, + AdminGetMeasuresParams, + AdminGetModerationLogParams, + AdminGetReportsParams, + AdminGetStatusesParams, + AdminMeasureKey, + AdminPerformAccountActionParams, + AdminUpdateAnnouncementParams, + AdminUpdateCustomEmojiParams, + AdminUpdateDomainBlockParams, + AdminUpdateReportParams, + AdminUpdateRuleParams, + AdminUpdateStatusParams, +} from '../params/admin'; +import type { EditStatusParams } from '../params/statuses'; +import type { PaginatedResponse } from '../responses'; + +type EmptyObject = Record; + +const paginatedPleromaAccounts = async ( + client: PlApiBaseClient, + params: { + query?: string; + filters?: string; + page?: number; + page_size: number; + tags?: Array; + actor_types?: Array; + name?: string; + email?: string; + }, +): Promise> => { + const response = await client.request('/api/v1/pleroma/admin/users', { params }); + + const adminAccounts = v.parse(filteredArray(adminAccountSchema), response.json?.users); + + return { + previous: params.page + ? () => paginatedPleromaAccounts(client, { ...params, page: params.page! - 1 }) + : null, + next: + response.json?.count > + params.page_size * ((params.page || 1) - 1) + response.json?.users?.length + ? () => paginatedPleromaAccounts(client, { ...params, page: (params.page || 0) + 1 }) + : null, + items: adminAccounts, + partial: response.status === 206, + total: response.json?.count, + }; +}; + +const paginatedPleromaReports = async ( + client: PlApiBaseClient, + params: { + state?: 'open' | 'closed' | 'resolved'; + limit?: number; + page?: number; + page_size: number; + }, +): Promise> => { + const response = await client.request('/api/v1/pleroma/admin/reports', { params }); + + return { + previous: params.page + ? () => paginatedPleromaReports(client, { ...params, page: params.page! - 1 }) + : null, + next: + response.json?.total > + params.page_size * ((params.page || 1) - 1) + response.json?.reports?.length + ? () => paginatedPleromaReports(client, { ...params, page: (params.page || 0) + 1 }) + : null, + items: v.parse(filteredArray(adminReportSchema), response.json?.reports), + partial: response.status === 206, + total: response.json?.total, + }; +}; + +const paginatedPleromaStatuses = async ( + client: PlApiBaseClient, + params: { + page_size?: number; + local_only?: boolean; + godmode?: boolean; + with_reblogs?: boolean; + page?: number; + }, +): Promise> => { + const response = await client.request('/api/v1/pleroma/admin/statuses', { params }); + + return { + previous: params.page + ? () => paginatedPleromaStatuses(client, { ...params, page: params.page! - 1 }) + : null, + next: response.json?.length + ? () => paginatedPleromaStatuses(client, { ...params, page: (params.page || 0) + 1 }) + : null, + items: v.parse(filteredArray(statusSchema), response.json), + partial: response.status === 206, + }; +}; + +const admin = (client: PlApiBaseClient) => { + const category = { + /** Perform moderation actions with accounts. */ + accounts: { + /** + * View accounts + * View all accounts, optionally matching certain criteria for filtering, up to 100 at a time. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#v2} + */ + getAccounts: (params?: AdminGetAccountsParams) => { + if (client.features.mastodonAdminV2) { + return client.paginatedGet('/api/v2/admin/accounts', { params }, adminAccountSchema); + } + + return paginatedPleromaAccounts( + client, + params + ? { + query: params.username, + name: params.display_name, + email: params.email, + filters: [ + params.origin === 'local' && 'local', + params.origin === 'remote' && 'external', + params.status === 'active' && 'active', + params.status === 'pending' && 'need_approval', + params.status === 'disabled' && 'deactivated', + params.permissions === 'staff' && 'is_admin', + params.permissions === 'staff' && 'is_moderator', + ] + .filter((filter) => filter) + .join(','), + page_size: 100, + } + : { page_size: 100 }, + ); + }, + + /** + * View a specific account + * View admin-level information about the given account. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#get-one} + */ + getAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}`); + } else { + response = await client.request(`/api/v1/pleroma/admin/users/${accountId}`); + } + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Approve a pending account + * Approve the given local account if it is currently pending approval. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#approve} + */ + approveAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}/approve`, { + method: 'POST', + }); + } else { + const account = await category.accounts.getAccount(accountId)!; + + response = await client.request('/api/v1/pleroma/admin/users/approve', { + method: 'PATCH', + body: { nicknames: [account.username] }, + }); + response.json = response.json?.users?.[0]; + } + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Reject a pending account + * Reject the given local account if it is currently pending approval. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#reject} + */ + rejectAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}/reject`, { + method: 'POST', + }); + } else { + const account = await category.accounts.getAccount(accountId)!; + + response = await client.request('/api/v1/pleroma/admin/users', { + method: 'DELETE', + body: { + nicknames: [account.username], + }, + }); + } + + return v.safeParse(adminAccountSchema, response.json).output || {}; + }, + + /** + * Requires features{@link Features.pleromaAdminAccounts}. + */ + createAccount: async (params: AdminCreateAccountParams) => { + const response = await client.request('/api/v1/pleroma/admin/users', { + method: 'POST', + body: { users: [params] }, + }); + + return v.parse( + v.object({ + nickname: v.string(), + email: v.string(), + }), + response.json[0]?.data, + ); + }, + + /** + * Delete an account + * Permanently delete data for a suspended accountusers + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#delete} + */ + deleteAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin || client.features.version.software === MITRA) { + response = await client.request(`/api/v1/admin/accounts/${accountId}`, { + method: 'DELETE', + }); + } else { + const account = await category.accounts.getAccount(accountId)!; + + response = await client.request('/api/v1/pleroma/admin/users', { + method: 'DELETE', + body: { + nicknames: [account.username], + }, + }); + } + + return v.safeParse(adminAccountSchema, response.json).output || {}; + }, + + /** + * Perform an action against an account + * Perform an action against an account and log this action in the moderation history. Also resolves any open reports against this account. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#action} + */ + performAccountAction: async ( + accountId: string, + type: AdminAccountAction, + params?: AdminPerformAccountActionParams, + ) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}/action`, { + body: { ...params, type }, + }); + } else { + const account = await category.accounts.getAccount(accountId)!; + + switch (type) { + case 'disable': + case 'suspend': + response = await client.request( + '/api/v1/pleroma/admin/users/deactivate', + { + body: { nicknames: [account.username] }, + }, + ); + break; + default: + response = { json: {} }; + break; + } + if (params?.report_id) await category.reports.resolveReport(params.report_id); + } + + return response.json; + }, + + /** + * Enable a currently disabled account + * Re-enable a local account whose login is currently disabled. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#enable} + */ + enableAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}/enable`, { + method: 'POST', + }); + } else { + const account = await category.accounts.getAccount(accountId)!; + response = await client.request('/api/v1/pleroma/admin/users/activate', { + method: 'PATCH', + body: { nicknames: [account.username] }, + }); + response.json = response.json?.users?.[0]; + } + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Unsilence an account + * Unsilence an account if it is currently silenced. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsilence} + */ + unsilenceAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/admin/accounts/${accountId}/unsilence`, { + method: 'POST', + }); + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Unsuspend an account + * Unsuspend a currently suspended account. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsuspend} + */ + unsuspendAccount: async (accountId: string) => { + let response; + + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/accounts/${accountId}/unsuspend`, { + method: 'POST', + }); + } else { + const { account } = await category.accounts.getAccount(accountId)!; + + response = await client.request('/api/v1/pleroma/admin/users/activate', { + method: 'PATCH', + body: { nicknames: [account!.acct] }, + }); + response.json = response.json?.users?.[0]; + } + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Unmark an account as sensitive + * Stops marking an account’s posts as sensitive, if it was previously flagged as sensitive. + * @see {@link https://docs.joinmastodon.org/methods/admin/accounts/#unsensitive} + */ + unsensitiveAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/admin/accounts/${accountId}/unsensitive`, { + method: 'POST', + }); + + return v.parse(adminAccountSchema, response.json); + }, + + /** + * Requires features{@link Features.pleromaAdminAccounts}. + */ + promoteToAdmin: async (accountId: string) => { + const { account } = await category.accounts.getAccount(accountId)!; + + await client.request( + '/api/v1/pleroma/admin/users/permission_group/moderator', + { + method: 'DELETE', + body: { nicknames: [account!.acct] }, + }, + ); + const response = await client.request( + '/api/v1/pleroma/admin/users/permission_group/admin', + { + method: 'POST', + body: { nicknames: [account!.acct] }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.pleromaAdminAccounts}. + */ + promoteToModerator: async (accountId: string) => { + const { account } = await category.accounts.getAccount(accountId)!; + + await client.request('/api/v1/pleroma/admin/users/permission_group/admin', { + method: 'DELETE', + body: { nicknames: [account!.acct] }, + }); + const response = await client.request( + '/api/v1/pleroma/admin/users/permission_group/moderator', + { + method: 'POST', + body: { nicknames: [account!.acct] }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.pleromaAdminAccounts}. + */ + demoteToUser: async (accountId: string) => { + const { account } = await category.accounts.getAccount(accountId)!; + + await client.request( + '/api/v1/pleroma/admin/users/permission_group/moderator', + { + method: 'DELETE', + body: { nicknames: [account!.acct] }, + }, + ); + const response = await client.request( + '/api/v1/pleroma/admin/users/permission_group/admin', + { + method: 'DELETE', + body: { nicknames: [account!.acct] }, + }, + ); + + return response.json; + }, + + /** + * Tag a user. + * + * Requires features{@link Features.pleromaAdminAccounts}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminuserssuggest} + */ + suggestUser: async (accountId: string) => { + const { account } = await category.accounts.getAccount(accountId)!; + + const response = await client.request('/api/v1/pleroma/admin/users/suggest', { + method: 'PATCH', + body: { nicknames: [account!.acct] }, + }); + + return response.json; + }, + + /** + * Untag a user. + * + * Requires features{@link Features.pleromaAdminAccounts}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminusersunsuggest} + */ + unsuggestUser: async (accountId: string) => { + const { account } = await category.accounts.getAccount(accountId)!; + + const response = await client.request( + '/api/v1/pleroma/admin/users/unsuggest', + { + method: 'PATCH', + body: { nicknames: [account!.acct] }, + }, + ); + + return response.json; + }, + + /** + * Tag a user. + * + * Requires features{@link Features.pleromaAdminAccounts}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#put-apiv1pleromaadminuserstag} + */ + tagUser: async (accountId: string, tags: Array) => { + const { account } = await category.accounts.getAccount(accountId)!; + + const response = await client.request('/api/v1/pleroma/admin/users/tag', { + method: 'PUT', + body: { nicknames: [account!.acct], tags }, + }); + + return response.json; + }, + + /** + * Untag a user. + * + * Requires features{@link Features.pleromaAdminAccounts}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminuserstag} + */ + untagUser: async (accountId: string, tags: Array) => { + const { account } = await category.accounts.getAccount(accountId)!; + + const response = await client.request('/api/v1/pleroma/admin/users/tag', { + method: 'DELETE', + body: { nicknames: [account!.acct], tags }, + }); + + return response.json; + }, + }, + + /** Disallow certain domains to federate. */ + domainBlocks: { + /** + * List all blocked domains + * Show information about all blocked domains. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#get} + */ + getDomainBlocks: (params?: AdminGetDomainBlocksParams) => + client.paginatedGet('/api/v1/admin/domain_blocks', { params }, adminDomainBlockSchema), + + /** + * Get a single blocked domain + * Show information about a single blocked domain. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#get-one} + */ + getDomainBlock: async (domainBlockId: string) => { + const response = await client.request(`/api/v1/admin/domain_blocks/${domainBlockId}`); + + return v.parse(adminDomainBlockSchema, response.json); + }, + + /** + * Block a domain from federating + * Add a domain to the list of domains blocked from federating. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#create} + */ + createDomainBlock: async (domain: string, params?: AdminCreateDomainBlockParams) => { + const response = await client.request('/api/v1/admin/domain_blocks', { + method: 'POST', + body: { ...params, domain }, + }); + + return v.parse(adminDomainBlockSchema, response.json); + }, + + /** + * Update a domain block + * Change parameters for an existing domain block. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#update} + */ + updateDomainBlock: async (domainBlockId: string, params?: AdminUpdateDomainBlockParams) => { + const response = await client.request(`/api/v1/admin/domain_blocks/${domainBlockId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(adminDomainBlockSchema, response.json); + }, + + /** + * Remove a domain block + * Lift a block against a domain. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_blocks/#delete} + */ + deleteDomainBlock: async (domainBlockId: string) => { + const response = await client.request( + `/api/v1/admin/domain_blocks/${domainBlockId}`, + { + method: 'DELETE', + }, + ); + + return response.json; + }, + }, + + /** Perform moderation actions with reports. */ + reports: { + /** + * View all reports + * View information about all reports. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#get} + */ + getReports: (params?: AdminGetReportsParams) => { + if (client.features.mastodonAdmin) { + if ( + params?.resolved === undefined && + (client.features.version.software === GOTOSOCIAL || + client.features.version.software === PLEROMA) + ) { + if (!params) params = {}; + params.resolved = false; + } + return client.paginatedGet('/api/v1/admin/reports', { params }, adminReportSchema); + } + + return paginatedPleromaReports(client, { + state: params?.resolved === true ? 'resolved' : 'open', + page_size: params?.limit || 100, + }); + }, + + /** + * View a single report + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#get-one} + */ + getReport: async (reportId: string) => { + let response; + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/reports/${reportId}`); + } else { + response = await client.request(`/api/v1/pleroma/admin/reports/${reportId}`); + } + + return v.parse(adminReportSchema, response.json); + }, + + /** + * Update a report + * Change metadata for a report. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#update} + */ + updateReport: async (reportId: string, params: AdminUpdateReportParams) => { + const response = await client.request(`/api/v1/admin/reports/${reportId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(adminReportSchema, response.json); + }, + + /** + * Assign report to self + * Claim the handling of this report to yourcategory. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#assign_tocategory} + */ + assignReportToSelf: async (reportId: string) => { + const response = await client.request( + `/api/v1/admin/reports/${reportId}/assign_tocategory`, + { + method: 'POST', + }, + ); + + return v.parse(adminReportSchema, response.json); + }, + + /** + * Unassign report + * Unassign a report so that someone else can claim it. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#unassign} + */ + unassignReport: async (reportId: string) => { + const response = await client.request(`/api/v1/admin/reports/${reportId}/unassign`, { + method: 'POST', + }); + + return v.parse(adminReportSchema, response.json); + }, + + /** + * Mark report as resolved + * + * Mark a report as resolved with no further action taken. + * + * `action_taken_comment` param requires features{@link Features.mastodonAdminResolveReportWithComment}. + * @param action_taken_comment Optional admin comment on the action taken in response to this report. Supported by GoToSocial only. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#resolve} + */ + resolveReport: async (reportId: string, action_taken_comment?: string) => { + let response; + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/reports/${reportId}/resolve`, { + method: 'POST', + body: { action_taken_comment }, + }); + } else { + response = await client.request(`/api/v1/pleroma/admin/reports/${reportId}`, { + method: 'PATCH', + body: { reports: [{ id: reportId, state: 'resolved' }] }, + }); + } + + return v.parse(adminReportSchema, response.json); + }, + + /** + * Reopen a closed report + * Reopen a currently closed report, if it is closed. + * @see {@link https://docs.joinmastodon.org/methods/admin/reports/#reopen} + */ + reopenReport: async (reportId: string) => { + let response; + if (client.features.mastodonAdmin) { + response = await client.request(`/api/v1/admin/reports/${reportId}/reopen`, { + method: 'POST', + }); + } else { + response = await client.request(`/api/v1/pleroma/admin/reports/${reportId}`, { + method: 'PATCH', + body: { reports: [{ id: reportId, state: 'open' }] }, + }); + } + + return v.parse(adminReportSchema, response.json); + }, + }, + + statuses: { + /** + * @param params Retrieves all latest statuses + * + * The params are subject to change in case Mastodon implements alike route. + * + * Requires features{@link Features.pleromaAdminStatuses}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminstatuses} + */ + getStatuses: (params?: AdminGetStatusesParams) => + paginatedPleromaStatuses(client, { + page_size: params?.limit || 100, + page: 1, + local_only: params?.local_only, + with_reblogs: params?.with_reblogs, + godmode: params?.with_private, + }), + + /** + * Show status by id + * + * Requires features{@link Features.pleromaAdminStatuses}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminstatusesid} + */ + getStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/pleroma/admin/statuses/${statusId}`); + + return v.parse(statusSchema, response.json); + }, + + /** + * Change the scope of an individual reported status + * + * Requires features{@link Features.pleromaAdminStatuses}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#put-apiv1pleromaadminstatusesid} + */ + updateStatus: async (statusId: string, params: AdminUpdateStatusParams) => { + const response = await client.request(`/api/v1/pleroma/admin/statuses/${statusId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Delete an individual reported status + * + * Requires features{@link Features.pleromaAdminStatuses}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminstatusesid} + */ + deleteStatus: async (statusId: string) => { + let response; + + if (client.features.version.software === MITRA) { + response = await client.request(`/api/v1/admin/posts/${statusId}`, { + method: 'DELETE', + }); + } else { + response = await client.request( + `/api/v1/pleroma/admin/statuses/${statusId}`, + { + method: 'DELETE', + }, + ); + } + + return response.json; + }, + + /** + * Requires features{@link Features.pleromaAdminStatusesRedact} + */ + redactStatus: async ( + statusId: string, + params: EditStatusParams & { overwrite?: boolean }, + ) => { + const response = await client.request(`/api/v1/pleroma/admin/statuses/${statusId}/redact`, { + method: 'PATCH', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Requires features{@link Features.pleromaAdminStatusesRedact} + */ + getStatusSource: async (statusId: string) => { + const response = await client.request(`/api/v1/pleroma/admin/statuses/${statusId}/source`); + + return v.parse(statusSourceSchema, response.json); + }, + }, + + trends: { + /** + * View trending links + * Links that have been shared more than others, including unapproved and unreviewed links. + * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#links} + */ + getTrendingLinks: async () => { + const response = await client.request('/api/v1/admin/trends/links'); + + return v.parse(filteredArray(trendsLinkSchema), response.json); + }, + + /** + * View trending statuses + * Statuses that have been interacted with more than others, including unapproved and unreviewed statuses. + * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#statuses} + */ + getTrendingStatuses: async () => { + const response = await client.request('/api/v1/admin/trends/statuses'); + + return v.parse(filteredArray(statusSchema), response.json); + }, + + /** + * View trending tags + * Tags that are being used more frequently within the past week, including unapproved and unreviewed tags. + * @see {@link https://docs.joinmastodon.org/methods/admin/trends/#tags} + */ + getTrendingTags: async () => { + const response = await client.request('/api/v1/admin/trends/links'); + + return v.parse(filteredArray(adminTagSchema), response.json); + }, + }, + + /** Block certain email addresses by their hash. */ + canonicalEmailBlocks: { + /** + * List all canonical email blocks + * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#get} + */ + getCanonicalEmailBlocks: (params?: AdminGetCanonicalEmailBlocks) => + client.paginatedGet( + '/api/v1/admin/canonical_email_blocks', + { params }, + adminCanonicalEmailBlockSchema, + ), + + /** + * Show a single canonical email block + * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#get-one} + */ + getCanonicalEmailBlock: async (canonicalEmailBlockId: string) => { + const response = await client.request( + `/api/v1/admin/canonical_email_blocks/${canonicalEmailBlockId}`, + ); + + return v.parse(adminCanonicalEmailBlockSchema, response.json); + }, + + /** + * Test + * Canoniocalize and hash an email address. + * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#test} + */ + testCanonicalEmailBlock: async (email: string) => { + const response = await client.request('/api/v1/admin/canonical_email_blocks/test', { + method: 'POST', + body: { email }, + }); + + return v.parse(filteredArray(adminCanonicalEmailBlockSchema), response.json); + }, + + /** + * Block a canonical email + * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#create} + */ + createCanonicalEmailBlock: async (email: string, canonical_email_hash?: string) => { + const response = await client.request('/api/v1/admin/canonical_email_blocks', { + method: 'POST', + body: { email, canonical_email_hash }, + }); + + return v.parse(filteredArray(adminCanonicalEmailBlockSchema), response.json); + }, + + /** + * Delete a canonical email block + * @see {@link https://docs.joinmastodon.org/methods/admin/canonical_email_blocks/#delete} + */ + deleteCanonicalEmailBlock: async (canonicalEmailBlockId: string) => { + const response = await client.request( + `/api/v1/admin/canonical_email_blocks/${canonicalEmailBlockId}`, + { method: 'DELETE' }, + ); + + return response.json; + }, + }, + + /** Obtain qualitative metrics about the server. */ + dimensions: { + /** + * Get dimensional data + * Obtain information about popularity of certain accounts, servers, languages, etc. + * @see {@link https://docs.joinmastodon.org/methods/admin/dimensions/#get} + */ + getDimensions: async (keys: AdminDimensionKey[], params?: AdminGetDimensionsParams) => { + const response = await client.request('/api/v1/admin/dimensions', { + method: 'POST', + params: { ...params, keys }, + }); + + return v.parse(filteredArray(adminDimensionSchema), response.json); + }, + }, + + /** Allow certain domains to federate. */ + domainAllows: { + /** + * List all allowed domains + * Show information about all allowed domains. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#get} + */ + getDomainAllows: (params?: AdminGetDomainAllowsParams) => + client.paginatedGet('/api/v1/admin/domain_allows', { params }, adminDomainAllowSchema), + + /** + * Get a single allowed domain + * Show information about a single allowed domain. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#get-one} + */ + getDomainAllow: async (domainAllowId: string) => { + const response = await client.request(`/api/v1/admin/domain_allows/${domainAllowId}`); + + return v.parse(adminDomainAllowSchema, response.json); + }, + + /** + * Allow a domain to federate + * Add a domain to the list of domains allowed to federate, to be used when the instance is in allow-list federation mode. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#create} + */ + createDomainAllow: async (domain: string) => { + const response = await client.request('/api/v1/admin/domain_allows', { + method: 'POST', + body: { domain }, + }); + + return v.parse(adminDomainAllowSchema, response.json); + }, + + /** + * Delete an allowed domain + * Delete a domain from the allowed domains list. + * @see {@link https://docs.joinmastodon.org/methods/admin/domain_allows/#delete} + */ + deleteDomainAllow: async (domainAllowId: string) => { + const response = await client.request( + `/api/v1/admin/domain_allows/${domainAllowId}`, + { + method: 'DELETE', + }, + ); + + return response.json; + }, + }, + + /** Disallow certain email domains from signing up. */ + emailDomainBlocks: { + /** + * List all blocked email domains + * Show information about all email domains blocked from signing up. + * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#get} + */ + getEmailDomainBlocks: (params?: AdminGetEmailDomainBlocksParams) => + client.paginatedGet( + '/api/v1/admin/email_domain_blocks', + { params }, + adminEmailDomainBlockSchema, + ), + + /** + * Get a single blocked email domain + * Show information about a single email domain that is blocked from signups. + * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#get-one} + */ + getEmailDomainBlock: async (emailDomainBlockId: string) => { + const response = await client.request( + `/api/v1/admin/email_domain_blocks/${emailDomainBlockId}`, + ); + + return v.parse(adminEmailDomainBlockSchema, response.json); + }, + + /** + * Block an email domain from signups + * Add a domain to the list of email domains blocked from signups. + * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#create} + */ + createEmailDomainBlock: async (domain: string) => { + const response = await client.request('/api/v1/admin/email_domain_blocks', { + method: 'POST', + body: { domain }, + }); + + return v.parse(adminEmailDomainBlockSchema, response.json); + }, + + /** + * Delete an email domain block + * Lift a block against an email domain. + * @see {@link https://docs.joinmastodon.org/methods/admin/email_domain_blocks/#delete} + */ + deleteEmailDomainBlock: async (emailDomainBlockId: string) => { + const response = await client.request( + `/api/v1/admin/email_domain_blocks/${emailDomainBlockId}`, + { method: 'DELETE' }, + ); + + return response.json; + }, + }, + + /** Disallow certain IP address ranges from signing up. */ + ipBlocks: { + /** + * List all IP blocks + * Show information about all blocked IP ranges. + * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#get} + */ + getIpBlocks: (params?: AdminGetIpBlocksParams) => + client.paginatedGet('/api/v1/admin/ip_blocks', { params }, adminIpBlockSchema), + + /** + * Get a single IP block + * Show information about a single IP block. + * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#get-one} + */ + getIpBlock: async (ipBlockId: string) => { + const response = await client.request(`/api/v1/admin/ip_blocks/${ipBlockId}`); + + return v.parse(adminIpBlockSchema, response.json); + }, + + /** + * Block an IP address range from signing up + * Add an IP address range to the list of IP blocks. + * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#create} + */ + createIpBlock: async (params: AdminCreateIpBlockParams) => { + const response = await client.request('/api/v1/admin/ip_blocks', { + method: 'POST', + body: params, + }); + + return v.parse(adminIpBlockSchema, response.json); + }, + + /** + * Update a domain block + * Change parameters for an existing IP block. + * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#update} + */ + updateIpBlock: async (ipBlockId: string, params: AdminCreateIpBlockParams) => { + const response = await client.request(`/api/v1/admin/ip_blocks/${ipBlockId}`, { + method: 'POST', + body: params, + }); + + return v.parse(adminIpBlockSchema, response.json); + }, + + /** + * Delete an IP block + * Lift a block against an IP range. + * @see {@link https://docs.joinmastodon.org/methods/admin/ip_blocks/#delete} + */ + deleteIpBlock: async (ipBlockId: string) => { + const response = await client.request(`/api/v1/admin/ip_blocks/${ipBlockId}`, { + method: 'DELETE', + }); + + return response.json; + }, + }, + + /** Obtain quantitative metrics about the server. */ + measures: { + /** + * Get measurable data + * Obtain quantitative metrics about the server. + * @see {@link https://docs.joinmastodon.org/methods/admin/measures/#get} + */ + getMeasures: async ( + keys: AdminMeasureKey[], + start_at: string, + end_at: string, + params?: AdminGetMeasuresParams, + ) => { + const response = await client.request('/api/v1/admin/measures', { + method: 'POST', + params: { ...params, keys, start_at, end_at }, + }); + + return v.parse(filteredArray(adminMeasureSchema), response.json); + }, + }, + + /** Show retention data over time. */ + retention: { + /** + * Calculate retention data + * + * Generate a retention data report for a given time period and bucket. + * @see {@link https://docs.joinmastodon.org/methods/admin/retention/#create} + */ + getRetention: async (start_at: string, end_at: string, frequency: 'day' | 'month') => { + const response = await client.request('/api/v1/admin/retention', { + method: 'POST', + params: { start_at, end_at, frequency }, + }); + + return v.parse(filteredArray(adminCohortSchema), response.json); + }, + }, + + announcements: { + /** + * List announcements + * + * Requires features{@link Features.pleromaAdminAnnouncements}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncements} + */ + getAnnouncements: async ( + params?: AdminGetAnnouncementsParams, + ): Promise> => { + const response = await client.request('/api/v1/pleroma/admin/announcements', { params }); + + const items = v.parse(filteredArray(adminAnnouncementSchema), response.json); + + return { + previous: null, + next: items.length + ? () => + category.announcements.getAnnouncements({ + ...params, + offset: (params?.offset || 0) + items.length, + }) + : null, + items, + partial: false, + }; + }, + + /** + * Display one announcement + * + * Requires features{@link Features.pleromaAdminAnnouncements}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminannouncementsid} + */ + getAnnouncement: async (announcementId: string) => { + const response = await client.request( + `/api/v1/pleroma/admin/announcements/${announcementId}`, + ); + + return v.parse(adminAnnouncementSchema, response.json); + }, + + /** + * Create an announcement + * + * Requires features{@link Features.pleromaAdminAnnouncements}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminannouncements} + */ + createAnnouncement: async (params: AdminCreateAnnouncementParams) => { + const response = await client.request('/api/v1/pleroma/admin/announcements', { + method: 'POST', + body: params, + }); + + return v.parse(adminAnnouncementSchema, response.json); + }, + + /** + * Change an announcement + * + * Requires features{@link Features.pleromaAdminAnnouncements}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminannouncementsid} + */ + updateAnnouncement: async (announcementId: string, params: AdminUpdateAnnouncementParams) => { + const response = await client.request( + `/api/v1/pleroma/admin/announcements/${announcementId}`, + { method: 'PATCH', body: params }, + ); + + return v.parse(adminAnnouncementSchema, response.json); + }, + + /** + * Delete an announcement + * + * Requires features{@link Features.pleromaAdminAnnouncements}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminannouncementsid} + */ + deleteAnnouncement: async (announcementId: string) => { + const response = await client.request( + `/api/v1/pleroma/admin/announcements/${announcementId}`, + { method: 'DELETE' }, + ); + + return response.json; + }, + }, + + domains: { + /** + * List of domains + * + * Requires features{@link Features.domains}. + */ + getDomains: async () => { + const response = await client.request('/api/v1/pleroma/admin/domains'); + + return v.parse(filteredArray(adminDomainSchema), response.json); + }, + + /** + * Create a domain + * + * Requires features{@link Features.domains}. + */ + createDomain: async (params: AdminCreateDomainParams) => { + const response = await client.request('/api/v1/pleroma/admin/domains', { + method: 'POST', + body: params, + }); + + return v.parse(adminDomainSchema, response.json); + }, + + /** + * Change domain publicity + * + * Requires features{@link Features.domains}. + */ + updateDomain: async (domainId: string, isPublic: boolean) => { + const response = await client.request(`/api/v1/pleroma/admin/domains/${domainId}`, { + method: 'PATCH', + body: { public: isPublic }, + }); + + return v.parse(adminDomainSchema, response.json); + }, + + /** + * Delete a domain + * + * Requires features{@link Features.domains}. + */ + deleteDomain: async (domainId: string) => { + const response = await client.request( + `/api/v1/pleroma/admin/domains/${domainId}`, + { + method: 'DELETE', + }, + ); + + return response.json; + }, + }, + + moderationLog: { + /** + * Get moderation log + * + * Requires features{@link Features.pleromaAdminModerationLog}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminmoderation_log} + */ + getModerationLog: async ({ limit, ...params }: AdminGetModerationLogParams = {}): Promise< + PaginatedResponse + > => { + const response = await client.request('/api/v1/pleroma/admin/moderation_log', { + params: { page_size: limit, ...params }, + }); + + const items = v.parse(filteredArray(adminModerationLogEntrySchema), response.json.items); + + return { + previous: + params.page && params.page > 1 + ? () => category.moderationLog.getModerationLog({ ...params, page: params.page! - 1 }) + : null, + next: + response.json.total > (params.page || 1) * (limit || 50) + ? () => + category.moderationLog.getModerationLog({ + ...params, + page: (params.page || 1) + 1, + }) + : null, + items, + partial: response.status === 206, + }; + }, + }, + + relays: { + /** + * List Relays + * + * Requires features{@link Features.pleromaAdminRelays}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrelay} + */ + getRelays: async () => { + const response = await client.request('/api/v1/pleroma/admin/relay'); + + return v.parse(filteredArray(adminRelaySchema), response.json); + }, + + /** + * Follow a Relay + * + * Requires features{@link Features.pleromaAdminRelays}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminrelay} + */ + followRelay: async (relayUrl: string) => { + const response = await client.request('/api/v1/pleroma/admin/relay', { + method: 'POST', + body: { relay_url: relayUrl }, + }); + + return v.parse(adminRelaySchema, response.json); + }, + + /** + * Unfollow a Relay + * + * Requires features{@link Features.pleromaAdminRelays}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminrelay} + */ + unfollowRelay: async (relayUrl: string, force = false) => { + const response = await client.request('/api/v1/pleroma/admin/relay', { + method: 'DELETE', + body: { relay_url: relayUrl, force }, + }); + + return v.parse(adminRelaySchema, response.json); + }, + }, + + rules: { + /** + * List rules + * + * Requires features{@link Features.adminRules}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#get-apiv1pleromaadminrules} + */ + getRules: async () => { + const response = await client.request( + client.features.version.software === GOTOSOCIAL + ? '/api/v1/admin/instance/rules' + : '/api/v1/pleroma/admin/rules', + ); + + return v.parse(filteredArray(adminRuleSchema), response.json); + }, + + /** + * Create a rule + * + * Requires features{@link Features.adminRules}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#post-apiv1pleromaadminrules} + */ + createRule: async (params: AdminCreateRuleParams) => { + const response = await client.request( + client.features.version.software === GOTOSOCIAL + ? '/api/v1/admin/instance/rules' + : '/api/v1/pleroma/admin/rules', + { method: 'POST', body: params }, + ); + + return v.parse(adminRuleSchema, response.json); + }, + + /** + * Update a rule + * + * Requires features{@link Features.adminRules}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#patch-apiv1pleromaadminrulesid} + */ + updateRule: async (ruleId: string, params: AdminUpdateRuleParams) => { + const response = await client.request( + `/api/v1/${client.features.version.software === GOTOSOCIAL ? 'admin/instance' : 'pleroma/admin'}/rules/${ruleId}`, + { method: 'PATCH', body: params }, + ); + + return v.parse(adminRuleSchema, response.json); + }, + + /** + * Delete a rule + * + * Requires features{@link Features.adminRules}. + * @see {@link https://docs.pleroma.social/backend/development/API/admin_api/#delete-apiv1pleromaadminrulesid} + */ + deleteRule: async (ruleId: string) => { + const response = await client.request( + `/api/v1/${client.features.version.software === GOTOSOCIAL ? 'admin/instance' : 'pleroma/admin'}/rules/${ruleId}`, + { method: 'DELETE' }, + ); + + return response.json; + }, + }, + + config: { + getPleromaConfig: async () => { + const response = await client.request('/api/v1/pleroma/admin/config'); + + return v.parse(pleromaConfigSchema, response.json); + }, + + updatePleromaConfig: async (params: PleromaConfig['configs']) => { + const response = await client.request('/api/v1/pleroma/admin/config', { + method: 'POST', + body: { configs: params }, + }); + + return v.parse(pleromaConfigSchema, response.json); + }, + }, + + customEmojis: { + /** + * View local and remote emojis available to/known by this instance. + * + * Requires features{@link Features.adminCustomEmojis}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + getCustomEmojis: (params: AdminGetCustomEmojisParams) => + client.paginatedGet('/api/v1/admin/custom_emojis', { params }, adminCustomEmojiSchema), + + /** + * Get the admin view of a single emoji. + * + * Requires features{@link Features.adminCustomEmojis}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + getCustomEmoji: async (emojiId: string) => { + const response = await client.request(`/api/v1/admin/custom_emojis/${emojiId}`); + + return v.parse(adminCustomEmojiSchema, response.json); + }, + + /** + * Get the admin view of a single emoji. + * + * Requires features{@link Features.adminCustomEmojis}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + createCustomEmoji: async (params: AdminCreateCustomEmojiParams) => { + const response = await client.request('/api/v1/admin/custom_emojis', { + method: 'POST', + body: params, + contentType: '', + }); + + return v.parse(adminCustomEmojiSchema, response.json); + }, + + updateCustomEmoji: async (emojiId: string, params: AdminUpdateCustomEmojiParams) => { + const response = await client.request(`/api/v1/admin/custom_emojis/${emojiId}`, { + method: 'PATCH', + body: params, + contentType: '', + }); + + return v.parse(adminCustomEmojiSchema, response.json); + }, + + /** + * Delete a **local** emoji with the given ID from the instance. + * + * Requires features{@link Features.adminCustomEmojis}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + deleteCustomEmoji: async (emojiId: string) => { + const response = await client.request(`/api/v1/admin/custom_emojis/${emojiId}`, { + method: 'DELETE', + }); + + return v.parse(adminCustomEmojiSchema, response.json); + }, + }, + }; + return category; +}; + +export { admin }; diff --git a/packages/pl-api/lib/client/announcements.ts b/packages/pl-api/lib/client/announcements.ts new file mode 100644 index 000000000..71a07d0db --- /dev/null +++ b/packages/pl-api/lib/client/announcements.ts @@ -0,0 +1,64 @@ +import * as v from 'valibot'; + +import { announcementSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; + +type EmptyObject = Record; + +const announcements = (client: PlApiBaseClient) => ({ + /** + * View all announcements + * See all currently active announcements set by admins. + * @see {@link https://docs.joinmastodon.org/methods/announcements/#get} + */ + getAnnouncements: async () => { + const response = await client.request('/api/v1/announcements'); + + return v.parse(filteredArray(announcementSchema), response.json); + }, + + /** + * Dismiss an announcement + * Allows a user to mark the announcement as read. + * @see {@link https://docs.joinmastodon.org/methods/announcements/#dismiss} + */ + dismissAnnouncements: async (announcementId: string) => { + const response = await client.request(`/api/v1/announcements/${announcementId}`, { + method: 'POST', + }); + + return response.json; + }, + + /** + * Add a reaction to an announcement + * React to an announcement with an emoji. + * @see {@link https://docs.joinmastodon.org/methods/announcements/#put-reactions} + */ + addAnnouncementReaction: async (announcementId: string, emoji: string) => { + const response = await client.request( + `/api/v1/announcements/${announcementId}/reactions/${emoji}`, + { method: 'PUT' }, + ); + + return response.json; + }, + + /** + * Remove a reaction from an announcement + * Undo a react emoji to an announcement. + * @see {@link https://docs.joinmastodon.org/methods/announcements/#delete-reactions} + */ + deleteAnnouncementReaction: async (announcementId: string, emoji: string) => { + const response = await client.request( + `/api/v1/announcements/${announcementId}/reactions/${emoji}`, + { method: 'DELETE' }, + ); + + return response.json; + }, +}); + +export { announcements }; diff --git a/packages/pl-api/lib/client/antennas.ts b/packages/pl-api/lib/client/antennas.ts new file mode 100644 index 000000000..26f88e763 --- /dev/null +++ b/packages/pl-api/lib/client/antennas.ts @@ -0,0 +1,336 @@ +import * as v from 'valibot'; + +import { accountSchema, antennaSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; +import type { CreateAntennaParams, UpdateAntennaParams } from '../params/antennas'; + +type EmptyObject = Record; + +const antennas = (client: PlApiBaseClient) => ({ + /** + * Requires features{@link Features.antennas}. + */ + fetchAntennas: async () => { + const response = await client.request('/api/v1/antennas'); + + return v.parse(filteredArray(antennaSchema), response.json); + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennas: async (antennaId: string) => { + const response = await client.request(`/api/v1/antennas/${antennaId}`); + + return v.parse(antennaSchema, response.json); + }, + + /** + * Requires features{@link Features.antennas}. + */ + createAntenna: async (params: CreateAntennaParams) => { + const response = await client.request('/api/v1/antennas', { method: 'POST', body: params }); + + return v.parse(antennaSchema, response.json); + }, + + /** + * Requires features{@link Features.antennas}. + */ + updateAntenna: async (antennaId: string, params: UpdateAntennaParams) => { + const response = await client.request(`/api/v1/antennas/${antennaId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(antennaSchema, response.json); + }, + + /** + * Requires features{@link Features.antennas}. + */ + deleteAntenna: async (antennaId: string) => { + const response = await client.request(`/api/v1/antennas/${antennaId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennaAccounts: (antennaId: string) => + client.paginatedGet(`/api/v1/antennas/${antennaId}/accounts`, {}, accountSchema), + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaAccounts: async (antennaId: string, accountIds: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/accounts`, { + method: 'POST', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaAccounts: async (antennaId: string, accountIds: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/accounts`, { + method: 'DELETE', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennaExcludedAccounts: (antennaId: string) => + client.paginatedGet(`/api/v1/antennas/${antennaId}/exclude_accounts`, {}, accountSchema), + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaExcludedAccounts: async (antennaId: string, accountIds: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_accounts`, + { + method: 'POST', + body: { account_ids: accountIds }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaExcludedAccounts: async (antennaId: string, accountIds: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_accounts`, + { + method: 'DELETE', + body: { account_ids: accountIds }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennaDomains: async (antennaId: string) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/domains`); + + return v.parse( + v.object({ + domains: filteredArray(v.string()), + exclude_domains: filteredArray(v.string()), + }), + response.json, + ); + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaDomains: async (antennaId: string, domains: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/domains`, { + method: 'POST', + body: { domains }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaDomains: async (antennaId: string, domains: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/domains`, { + method: 'DELETE', + body: { domains }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaExcludedDomains: async (antennaId: string, domains: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_domains`, + { + method: 'POST', + body: { domains }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaExcludedDomains: async (antennaId: string, domains: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_domains`, + { + method: 'DELETE', + body: { domains }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennaKeywords: async (antennaId: string) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/keywords`); + + return v.parse( + v.object({ + keywords: filteredArray(v.string()), + exclude_keywords: filteredArray(v.string()), + }), + response.json, + ); + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaKeywords: async (antennaId: string, keywords: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/keywords`, { + method: 'POST', + body: { keywords }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaKeywords: async (antennaId: string, keywords: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/keywords`, { + method: 'DELETE', + body: { keywords }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaExcludedKeywords: async (antennaId: string, keywords: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_keywords`, + { + method: 'POST', + body: { keywords }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaExcludedKeywords: async (antennaId: string, keywords: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_keywords`, + { + method: 'DELETE', + body: { keywords }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + getAntennaTags: async (antennaId: string) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/tags`); + + return v.parse( + v.object({ + tags: filteredArray(v.string()), + exclude_tags: filteredArray(v.string()), + }), + response.json, + ); + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaTags: async (antennaId: string, tags: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/tags`, { + method: 'POST', + body: { tags }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaTags: async (antennaId: string, tags: Array) => { + const response = await client.request(`/api/v1/antennas/${antennaId}/tags`, { + method: 'DELETE', + body: { tags }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + addAntennaExcludedTags: async (antennaId: string, tags: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_tags`, + { + method: 'POST', + body: { tags }, + }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.antennas}. + */ + removeAntennaExcludedTags: async (antennaId: string, tags: Array) => { + const response = await client.request( + `/api/v1/antennas/${antennaId}/exclude_tags`, + { + method: 'DELETE', + body: { tags }, + }, + ); + + return response.json; + }, +}); + +export { antennas }; diff --git a/packages/pl-api/lib/client/apps.ts b/packages/pl-api/lib/client/apps.ts new file mode 100644 index 000000000..1b48f336c --- /dev/null +++ b/packages/pl-api/lib/client/apps.ts @@ -0,0 +1,33 @@ +import * as v from 'valibot'; + +import { applicationSchema, credentialApplicationSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { CreateApplicationParams } from '../params/apps'; + +/** Register client applications that can be used to obtain OAuth tokens. */ +const apps = (client: PlApiBaseClient) => ({ + /** + * Create an application + * Create a new application to obtain OAuth2 credentials. + * @see {@link https://docs.joinmastodon.org/methods/apps/#create} + */ + createApplication: async (params: CreateApplicationParams) => { + const response = await client.request('/api/v1/apps', { method: 'POST', body: params }); + + return v.parse(credentialApplicationSchema, response.json); + }, + + /** + * Verify your app works + * Confirm that the app’s OAuth2 credentials work. + * @see {@link https://docs.joinmastodon.org/methods/apps/#verify_credentials} + */ + verifyApplication: async () => { + const response = await client.request('/api/v1/apps/verify_credentials'); + + return v.parse(applicationSchema, response.json); + }, +}); + +export { apps }; diff --git a/packages/pl-api/lib/client/async-refreshes.ts b/packages/pl-api/lib/client/async-refreshes.ts new file mode 100644 index 000000000..4d7b437ed --- /dev/null +++ b/packages/pl-api/lib/client/async-refreshes.ts @@ -0,0 +1,20 @@ +import * as v from 'valibot'; + +import { asyncRefreshSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; + +/** Experimental async refreshes API methods */ +const asyncRefreshes = (client: PlApiBaseClient) => ({ + /** + * Get Status of Async Refresh + * @see {@link https://docs.joinmastodon.org/methods/async_refreshes/#show} + */ + show: async (id: string) => { + const response = await client.request(`/api/v1_alpha/async_refreshes/${id}`); + + return v.parse(asyncRefreshSchema, response.json); + }, +}); + +export { asyncRefreshes }; diff --git a/packages/pl-api/lib/client/chats.ts b/packages/pl-api/lib/client/chats.ts new file mode 100644 index 000000000..0b9081841 --- /dev/null +++ b/packages/pl-api/lib/client/chats.ts @@ -0,0 +1,117 @@ +import * as v from 'valibot'; + +import { chatMessageSchema, chatSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateChatMessageParams, + GetChatMessagesParams, + GetChatsParams, +} from '../params/chats'; + +/** @see {@link https://docs.pleroma.social/backend/development/API/chats} */ +const chats = (client: PlApiBaseClient) => ({ + /** + * create or get an existing Chat for a certain recipient + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#creating-or-getting-a-chat} + */ + createChat: async (accountId: string) => { + const response = await client.request(`/api/v1/pleroma/chats/by-account-id/${accountId}`, { + method: 'POST', + }); + + return v.parse(chatSchema, response.json); + }, + + /** + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#creating-or-getting-a-chat} + */ + getChat: async (chatId: string) => { + const response = await client.request(`/api/v1/pleroma/chats/${chatId}`); + + return v.parse(chatSchema, response.json); + }, + + /** + * Marking a chat as read + * mark a number of messages in a chat up to a certain message as read + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#marking-a-chat-as-read} + */ + markChatAsRead: async (chatId: string, last_read_id: string) => { + const response = await client.request(`/api/v1/pleroma/chats/${chatId}/read`, { + method: 'POST', + body: { last_read_id }, + }); + + return v.parse(chatSchema, response.json); + }, + + /** + * Marking a single chat message as read + * To set the `unread` property of a message to `false` + * https://docs.pleroma.social/backend/development/API/chats/#marking-a-single-chat-message-as-read + */ + markChatMessageAsRead: async (chatId: string, chatMessageId: string) => { + const response = await client.request( + `/api/v1/pleroma/chats/${chatId}/messages/${chatMessageId}/read`, + { method: 'POST' }, + ); + + return v.parse(chatSchema, response.json); + }, + + /** + * Getting a list of Chats + * This will return a list of chats that you have been involved in, sorted by their last update (so new chats will be at the top). + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#getting-a-list-of-chats} + */ + getChats: (params?: GetChatsParams) => + client.paginatedGet('/api/v2/pleroma/chats', { params }, chatSchema), + + /** + * Getting the messages for a Chat + * For a given Chat id, you can get the associated messages with + */ + getChatMessages: (chatId: string, params?: GetChatMessagesParams) => + client.paginatedGet(`/api/v1/pleroma/chats/${chatId}/messages`, { params }, chatMessageSchema), + + /** + * Posting a chat message + * Posting a chat message for given Chat id works like this: + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#posting-a-chat-message} + */ + createChatMessage: async (chatId: string, params: CreateChatMessageParams) => { + const response = await client.request(`/api/v1/pleroma/chats/${chatId}/messages`, { + method: 'POST', + body: params, + }); + + return v.parse(chatMessageSchema, response.json); + }, + + /** + * Deleting a chat message + * Deleting a chat message for given Chat id works like this: + * @see {@link https://docs.pleroma.social/backend/development/API/chats/#deleting-a-chat-message} + */ + deleteChatMessage: async (chatId: string, messageId: string) => { + const response = await client.request(`/api/v1/pleroma/chats/${chatId}/messages/${messageId}`, { + method: 'DELETE', + }); + + return v.parse(chatMessageSchema, response.json); + }, + + /** + * Deleting a chat + * + * Requires features{@link Features.chatsDelete}. + */ + deleteChat: async (chatId: string) => { + const response = await client.request(`/api/v1/pleroma/chats/${chatId}`, { method: 'DELETE' }); + + return v.parse(chatSchema, response.json); + }, +}); + +export { chats }; diff --git a/packages/pl-api/lib/client/circles.ts b/packages/pl-api/lib/client/circles.ts new file mode 100644 index 000000000..000cd56df --- /dev/null +++ b/packages/pl-api/lib/client/circles.ts @@ -0,0 +1,101 @@ +import * as v from 'valibot'; + +import { accountSchema, circleSchema, statusSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; +import type { GetCircleAccountsParams, GetCircleStatusesParams } from '../params/circles'; + +type EmptyObject = Record; + +const circles = (client: PlApiBaseClient) => ({ + /** + * Requires features{@link Features.circles}. + */ + fetchCircles: async () => { + const response = await client.request('/api/v1/circles'); + + return v.parse(filteredArray(circleSchema), response.json); + }, + + /** + * Requires features{@link Features.circles}. + */ + getCircle: async (circleId: string) => { + const response = await client.request(`/api/v1/circles/${circleId}`); + + return v.parse(circleSchema, response.json); + }, + + /** + * Requires features{@link Features.circles}. + */ + createCircle: async (title: string) => { + const response = await client.request('/api/v1/circles', { method: 'POST', body: { title } }); + + return v.parse(circleSchema, response.json); + }, + + /** + * Requires features{@link Features.circles}. + */ + updateCircle: async (circleId: string, title: string) => { + const response = await client.request(`/api/v1/circles/${circleId}`, { + method: 'PUT', + body: { title }, + }); + + return v.parse(circleSchema, response.json); + }, + + /** + * Requires features{@link Features.circles}. + */ + deleteCircle: async (circleId: string) => { + const response = await client.request(`/api/v1/circles/${circleId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * View accounts in a circle + * Requires features{@link Features.circles}. + */ + getCircleAccounts: (circleId: string, params?: GetCircleAccountsParams) => + client.paginatedGet(`/api/v1/circles/${circleId}/accounts`, { params }, accountSchema), + + /** + * Add accounts to a circle + * Add accounts to the given circle. Note that the user must be following these accounts. + * Requires features{@link Features.circles}. + */ + addCircleAccounts: async (circleId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/circles/${circleId}/accounts`, { + method: 'POST', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** + * Remove accounts from circle + * Remove accounts from the given circle. + * Requires features{@link Features.circles}. + */ + deleteCircleAccounts: async (circleId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/circles/${circleId}/accounts`, { + method: 'DELETE', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + getCircleStatuses: (circleId: string, params: GetCircleStatusesParams) => + client.paginatedGet(`/api/v1/circles/${circleId}/statuses`, { params }, statusSchema), +}); + +export { circles }; diff --git a/packages/pl-api/lib/client/drive.ts b/packages/pl-api/lib/client/drive.ts new file mode 100644 index 000000000..469561021 --- /dev/null +++ b/packages/pl-api/lib/client/drive.ts @@ -0,0 +1,132 @@ +import * as v from 'valibot'; + +import { driveFileSchema, driveFolderSchema, driveStatusSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { UpdateFileParams } from '../params/drive'; + +type EmptyObject = Record; + +const drive = (client: PlApiBaseClient) => ({ + getDrive: async () => { + await client.getIceshrimpAccessToken(); + + const response = await client.request('/api/iceshrimp/drive/folder'); + + return v.parse(driveFolderSchema, response.json); + }, + + getFolder: async (id: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/folder/${id}`); + + return v.parse(driveFolderSchema, response.json); + }, + + createFolder: async (name: string, parentId?: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request('/api/iceshrimp/drive/folder', { + method: 'POST', + body: { name, parentId: parentId || null }, + }); + + return v.parse(driveFolderSchema, response.json); + }, + + updateFolder: async (id: string, name: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/folder/${id}`, { + method: 'PUT', + body: name, + }); + + return v.parse(driveFolderSchema, response.json); + }, + + deleteFolder: async (id: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/folder/${id}`, { + method: 'DELETE', + }); + + return response; + }, + + moveFolder: async (id: string, targetFolderId?: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/folder/${id}/move`, { + method: 'POST', + body: { folderId: targetFolderId || null }, + }); + + return v.parse(driveFolderSchema, response.json); + }, + + getFile: async (id: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/${id}`); + + return v.parse(driveFileSchema, response.json); + }, + + createFile: async (file: File, folderId?: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request('/api/iceshrimp/drive', { + method: 'POST', + body: { file }, + params: { folderId }, + contentType: '', + }); + + return v.parse(driveFileSchema, response.json); + }, + + updateFile: async (id: string, params: UpdateFileParams) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/${id}`, { + method: 'PATCH', + body: params, + }); + + return v.parse(driveFileSchema, response.json); + }, + + deleteFile: async (id: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request>(`/api/iceshrimp/drive/${id}`, { + method: 'DELETE', + }); + + return response; + }, + + moveFile: async (id: string, targetFolderId?: string) => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(`/api/iceshrimp/drive/${id}/move`, { + method: 'POST', + body: { folderId: targetFolderId || null }, + }); + + return v.parse(driveFileSchema, response.json); + }, + + getDriveStatus: async () => { + await client.getIceshrimpAccessToken(); + + const response = await client.request('/api/iceshrimp/drive/status'); + + return v.parse(driveStatusSchema, response.json); + }, +}); + +export { drive }; diff --git a/packages/pl-api/lib/client/emails.ts b/packages/pl-api/lib/client/emails.ts new file mode 100644 index 000000000..274af74e0 --- /dev/null +++ b/packages/pl-api/lib/client/emails.ts @@ -0,0 +1,16 @@ +import type { PlApiBaseClient } from '../client-base'; + +type EmptyObject = Record; + +const emails = (client: PlApiBaseClient) => ({ + resendConfirmationEmail: async (email: string) => { + const response = await client.request('/api/v1/emails/confirmations', { + method: 'POST', + body: { email }, + }); + + return response.json; + }, +}); + +export { emails }; diff --git a/packages/pl-api/lib/client/events.ts b/packages/pl-api/lib/client/events.ts new file mode 100644 index 000000000..846e9896f --- /dev/null +++ b/packages/pl-api/lib/client/events.ts @@ -0,0 +1,141 @@ +import * as v from 'valibot'; + +import { accountSchema, statusSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateEventParams, + EditEventParams, + GetEventParticipationRequestsParams, + GetEventParticipationsParams, + GetJoinedEventsParams, +} from '../params/events'; + +const events = (client: PlApiBaseClient) => ({ + /** + * Creates an event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events} + */ + createEvent: async (params: CreateEventParams) => { + const response = await client.request('/api/v1/pleroma/events', { + method: 'POST', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Edits an event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id} + */ + editEvent: async (statusId: string, params: EditEventParams) => { + const response = await client.request(`/api/v1/pleroma/events/${statusId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Gets user's joined events + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-joined_events} + */ + getJoinedEvents: (state?: 'pending' | 'reject' | 'accept', params?: GetJoinedEventsParams) => + client.paginatedGet( + '/api/v1/pleroma/events/joined_events', + { params: { ...params, state } }, + statusSchema, + ), + + /** + * Gets event participants + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participations} + */ + getEventParticipations: (statusId: string, params?: GetEventParticipationsParams) => + client.paginatedGet( + `/api/v1/pleroma/events/${statusId}/participations`, + { params }, + accountSchema, + ), + + /** + * Gets event participation requests + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests} + */ + getEventParticipationRequests: (statusId: string, params?: GetEventParticipationRequestsParams) => + client.paginatedGet( + `/api/v1/pleroma/events/${statusId}/participation_requests`, + { params }, + v.object({ + account: accountSchema, + participation_message: v.fallback(v.string(), ''), + }), + ), + + /** + * Accepts user to the event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests-participant_id-authorize} + */ + acceptEventParticipationRequest: async (statusId: string, accountId: string) => { + const response = await client.request( + `/api/v1/pleroma/events/${statusId}/participation_requests/${accountId}/authorize`, + { method: 'POST' }, + ); + + return v.parse(statusSchema, response.json); + }, + + /** + * Rejects user from the event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-participation_requests-participant_id-reject} + */ + rejectEventParticipationRequest: async (statusId: string, accountId: string) => { + const response = await client.request( + `/api/v1/pleroma/events/${statusId}/participation_requests/${accountId}/reject`, + { method: 'POST' }, + ); + + return v.parse(statusSchema, response.json); + }, + + /** + * Joins the event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-join} + */ + joinEvent: async (statusId: string, participation_message?: string) => { + const response = await client.request(`/api/v1/pleroma/events/${statusId}/join`, { + method: 'POST', + body: { participation_message }, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Leaves the event + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#api-v1-pleroma-events-id-leave} + */ + leaveEvent: async (statusId: string) => { + const response = await client.request(`/api/v1/pleroma/events/${statusId}/leave`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Event ICS file + * @see {@link https://codeberg.org/mkljczk/nicolex/src/branch/develop/docs/development/API/pleroma_api.md#event-ics-file} + */ + getEventIcs: async (statusId: string) => { + const response = await client.request(`/api/v1/pleroma/events/${statusId}/ics`, { + contentType: '', + }); + + return response.data; + }, +}); + +export { events }; diff --git a/packages/pl-api/lib/client/experimental.ts b/packages/pl-api/lib/client/experimental.ts new file mode 100644 index 000000000..728a7f32d --- /dev/null +++ b/packages/pl-api/lib/client/experimental.ts @@ -0,0 +1,296 @@ +import * as v from 'valibot'; + +import { + accountSchema, + groupMemberSchema, + groupRelationshipSchema, + groupSchema, + statusSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { PIXELFED } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { GroupRole } from '../entities'; +import type { AdminGetGroupsParams } from '../params/admin'; +import type { + CreateGroupParams, + GetGroupBlocksParams, + GetGroupMembershipRequestsParams, + GetGroupMembershipsParams, + UpdateGroupParams, +} from '../params/groups'; + +type EmptyObject = Record; + +/** Routes that are not part of any stable release */ +const experimental = (client: PlApiBaseClient) => { + const category = { + admin: { + /** @see {@link https://github.com/mastodon/mastodon/pull/19059} */ + groups: { + /** list groups known to the instance. Mimics the interface of `/api/v1/admin/accounts` */ + getGroups: async (params?: AdminGetGroupsParams) => { + const response = await client.request('/api/v1/admin/groups', { params }); + + return v.parse(filteredArray(groupSchema), response.json); + }, + + /** return basic group information */ + getGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/admin/groups/${groupId}`); + + return v.parse(groupSchema, response.json); + }, + + /** suspends a group */ + suspendGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/admin/groups/${groupId}/suspend`, { + method: 'POST', + }); + + return v.parse(groupSchema, response.json); + }, + + /** lift a suspension */ + unsuspendGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/admin/groups/${groupId}/unsuspend`, { + method: 'POST', + }); + + return v.parse(groupSchema, response.json); + }, + + /** deletes an already-suspended group */ + deleteGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/admin/groups/${groupId}`, { + method: 'DELETE', + }); + + return v.parse(groupSchema, response.json); + }, + }, + }, + + /** @see {@link https://github.com/mastodon/mastodon/pull/19059} */ + groups: { + /** returns an array of `Group` entities the current user is a member of */ + getGroups: async () => { + let response; + if (client.features.version.software === PIXELFED) { + response = await client.request('/api/v0/groups/self/list'); + } else { + response = await client.request('/api/v1/groups'); + } + + return v.parse(filteredArray(groupSchema), response.json); + }, + + /** create a group with the given attributes (`display_name`, `note`, `avatar` and `header`). Sets the user who made the request as group administrator */ + createGroup: async (params: CreateGroupParams) => { + let response; + + if (client.features.version.software === PIXELFED) { + response = await client.request('/api/v0/groups/create', { + method: 'POST', + body: { + ...params, + name: params.display_name, + description: params.note, + membership: 'public', + }, + contentType: params.avatar || params.header ? '' : undefined, + }); + + if (response.json?.id) { + return category.groups.getGroup(response.json.id); + } + } else { + response = await client.request('/api/v1/groups', { + method: 'POST', + body: params, + contentType: params.avatar || params.header ? '' : undefined, + }); + } + + return v.parse(groupSchema, response.json); + }, + + /** returns the `Group` entity describing a given group */ + getGroup: async (groupId: string) => { + let response; + + if (client.features.version.software === PIXELFED) { + response = await client.request(`/api/v0/groups/${groupId}`); + } else { + response = await client.request(`/api/v1/groups/${groupId}`); + } + + return v.parse(groupSchema, response.json); + }, + + /** update group attributes (`display_name`, `note`, `avatar` and `header`) */ + updateGroup: async (groupId: string, params: UpdateGroupParams) => { + const response = await client.request(`/api/v1/groups/${groupId}`, { + method: 'PUT', + body: params, + contentType: params.avatar || params.header ? '' : undefined, + }); + + return v.parse(groupSchema, response.json); + }, + + /** irreversibly deletes the group */ + deleteGroup: async (groupId: string) => { + let response; + + if (client.features.version.software === PIXELFED) { + response = await client.request('/api/v0/groups/delete', { + method: 'POST', + params: { gid: groupId }, + }); + } else { + response = await client.request(`/api/v1/groups/${groupId}`, { + method: 'DELETE', + }); + } + + return response.json; + }, + + /** Has an optional role attribute that can be used to filter by role (valid roles are `"admin"`, `"moderator"`, `"user"`). */ + getGroupMemberships: ( + groupId: string, + role?: GroupRole, + params?: GetGroupMembershipsParams, + ) => + client.paginatedGet( + client.features.version.software === PIXELFED + ? `/api/v0/groups/members/list?gid=${groupId}` + : `/api/v1/groups/${groupId}/memberships`, + { params: { ...params, role } }, + groupMemberSchema, + ), + + /** returns an array of `Account` entities representing pending requests to join a group */ + getGroupMembershipRequests: (groupId: string, params?: GetGroupMembershipRequestsParams) => + client.paginatedGet( + client.features.version.software === PIXELFED + ? `/api/v0/groups/members/requests?gid=${groupId}` + : `/api/v1/groups/${groupId}/membership_requests`, + { params }, + accountSchema, + ), + + /** accept a pending request to become a group member */ + acceptGroupMembershipRequest: async (groupId: string, accountId: string) => { + const response = await client.request( + `/api/v1/groups/${groupId}/membership_requests/${accountId}/authorize`, + { method: 'POST' }, + ); + + return response.json; + }, + + /** reject a pending request to become a group member */ + rejectGroupMembershipRequest: async (groupId: string, accountId: string) => { + const response = await client.request( + `/api/v1/groups/${groupId}/membership_requests/${accountId}/reject`, + { method: 'POST' }, + ); + + return response.json; + }, + + /** delete a group post (actually marks it as `revoked` if it is a local post) */ + deleteGroupStatus: async (groupId: string, statusId: string) => { + const response = await client.request(`/api/v1/groups/${groupId}/statuses/${statusId}`, { + method: 'DELETE', + }); + + return v.parse(statusSchema, response.json); + }, + + /** list accounts blocked from interacting with the group */ + getGroupBlocks: (groupId: string, params?: GetGroupBlocksParams) => + client.paginatedGet(`/api/v1/groups/${groupId}/blocks`, { params }, accountSchema), + + /** block one or more users. If they were in the group, they are also kicked of it */ + blockGroupUsers: async (groupId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/groups/${groupId}/blocks`, { + method: 'POST', + params: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** block one or more users. If they were in the group, they are also kicked of it */ + unblockGroupUsers: async (groupId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/groups/${groupId}/blocks`, { + method: 'DELETE', + params: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** joins (or request to join) a given group */ + joinGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/groups/${groupId}/join`, { method: 'POST' }); + + return v.parse(groupRelationshipSchema, response.json); + }, + + /** leaves a given group */ + leaveGroup: async (groupId: string) => { + const response = await client.request(`/api/v1/groups/${groupId}/leave`, { + method: 'POST', + }); + + return v.parse(groupRelationshipSchema, response.json); + }, + + /** kick one or more group members */ + kickGroupUsers: async (groupId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/groups/${groupId}/kick`, { + method: 'POST', + params: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** promote one or more accounts to role `new_role`. An error is returned if any of those accounts has a higher role than `new_role` already, or if the role is higher than the issuing user's. Valid roles are `admin`, and `moderator` and `user`. */ + promoteGroupUsers: async (groupId: string, accountIds: string[], role: GroupRole) => { + const response = await client.request(`/api/v1/groups/${groupId}/promote`, { + method: 'POST', + params: { account_ids: accountIds, role }, + }); + + return v.parse(filteredArray(groupMemberSchema), response.json); + }, + + /** demote one or more accounts to role `new_role`. Returns an error unless every of the target account has a strictly lower role than the user (you cannot demote someone with the same role as you), or if any target account already has a role lower than `new_role`. Valid roles are `admin`, `moderator` and `user`. */ + demoteGroupUsers: async (groupId: string, accountIds: string[], role: GroupRole) => { + const response = await client.request(`/api/v1/groups/${groupId}/demote`, { + method: 'POST', + params: { account_ids: accountIds, role }, + }); + + return v.parse(filteredArray(groupMemberSchema), response.json); + }, + + getGroupRelationships: async (groupIds: string[]) => { + const response = await client.request('/api/v1/groups/relationships', { + params: { id: groupIds }, + }); + + return v.parse(filteredArray(groupRelationshipSchema), response.json); + }, + }, + }; + return category; +}; + +export { experimental }; diff --git a/packages/pl-api/lib/client/filtering.ts b/packages/pl-api/lib/client/filtering.ts new file mode 100644 index 000000000..5e8610521 --- /dev/null +++ b/packages/pl-api/lib/client/filtering.ts @@ -0,0 +1,373 @@ +import * as v from 'valibot'; + +import { + blockedAccountSchema, + filterKeywordSchema, + filterSchema, + filterStatusSchema, + mutedAccountSchema, + relationshipSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + BlockAccountParams, + CreateFilterParams, + GetBlocksParams, + GetDomainBlocksParams, + GetMutesParams, + MuteAccountParams, + UpdateFilterParams, +} from '../params/filtering'; + +type EmptyObject = Record; + +const filtering = (client: PlApiBaseClient) => ({ + /** + * Block account + * Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline) + * @see {@link https://docs.joinmastodon.org/methods/accounts/#block} + * `duration` parameter requires features{@link Features.blocksDuration}. + */ + blockAccount: async (accountId: string, params?: BlockAccountParams) => { + const response = await client.request(`/api/v1/accounts/${accountId}/block`, { + method: 'POST', + body: params, + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Unblock account + * Unblock the given account. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#unblock} + */ + unblockAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/unblock`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Mute account + * Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline). + * + * Requires features{@link Features.mutes}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#mute} + */ + muteAccount: async (accountId: string, params?: MuteAccountParams) => { + const response = await client.request(`/api/v1/accounts/${accountId}/mute`, { + method: 'POST', + body: params, + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Unmute account + * Unmute the given account. + * + * Requires features{@link Features.mutes}. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#unmute} + */ + unmuteAccount: async (accountId: string) => { + const response = await client.request(`/api/v1/accounts/${accountId}/unmute`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * View muted accounts + * Accounts the user has muted. + * + * Requires features{@link Features.mutes}. + * @see {@link https://docs.joinmastodon.org/methods/mutes/#get} + */ + getMutes: (params?: GetMutesParams) => + client.paginatedGet('/api/v1/mutes', { params }, mutedAccountSchema), + + /** + * View blocked users + * @see {@link https://docs.joinmastodon.org/methods/blocks/#get} + */ + getBlocks: (params?: GetBlocksParams) => + client.paginatedGet('/api/v1/blocks', { params }, blockedAccountSchema), + + /** + * Get domain blocks + * View domains the user has blocked. + * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#get} + */ + getDomainBlocks: (params?: GetDomainBlocksParams) => + client.paginatedGet('/api/v1/domain_blocks', { params }, v.string()), + + /** + * Block a domain + * Block a domain to: + * - hide all public posts from it + * - hide all notifications from it + * - remove all followers from it + * - prevent following new users from it (but does not remove existing follows) + * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#block} + */ + blockDomain: async (domain: string) => { + const response = await client.request('/api/v1/domain_blocks', { + method: 'POST', + body: { domain }, + }); + + return response.json; + }, + + /** + * Unblock a domain + * Remove a domain block, if it exists in the user’s array of blocked domains. + * @see {@link https://docs.joinmastodon.org/methods/domain_blocks/#unblock} + */ + unblockDomain: async (domain: string) => { + const response = await client.request('/api/v1/domain_blocks', { + method: 'DELETE', + body: { domain }, + }); + + return response.json; + }, + + /** + * View all filters + * Obtain a list of all filter groups for the current user. + * + * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#get} + */ + getFilters: async () => { + const response = await client.request( + client.features.filtersV2 ? '/api/v2/filters' : '/api/v1/filters', + ); + + return v.parse(filteredArray(filterSchema), response.json); + }, + + /** + * View a specific filter + * Obtain a single filter group owned by the current user. + * + * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#get-one} + */ + getFilter: async (filterId: string) => { + const response = await client.request( + client.features.filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, + ); + + return v.parse(filterSchema, response.json); + }, + + /** + * Create a filter + * Create a filter group with the given parameters. + * + * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#create} + */ + createFilter: async (params: CreateFilterParams) => { + const { filtersV2 } = client.features; + const response = await client.request(filtersV2 ? '/api/v2/filters' : '/api/v1/filters', { + method: 'POST', + body: filtersV2 + ? params + : { + phrase: params.keywords_attributes[0]?.keyword, + context: params.context, + irreversible: params.filter_action === 'hide', + whole_word: params.keywords_attributes[0]?.whole_word, + expires_in: params.expires_in, + }, + }); + + return v.parse(filterSchema, response.json); + }, + + /** + * Update a filter + * Update a filter group with the given parameters. + * + * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#update} + */ + updateFilter: async (filterId: string, params: UpdateFilterParams) => { + const { filtersV2 } = client.features; + const response = await client.request( + filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, + { + method: 'PUT', + body: filtersV2 + ? params + : { + phrase: params.keywords_attributes?.[0]?.keyword, + context: params.context, + irreversible: params.filter_action === 'hide', + whole_word: params.keywords_attributes?.[0]?.whole_word, + expires_in: params.expires_in, + }, + }, + ); + + return v.parse(filterSchema, response.json); + }, + + /** + * Delete a filter + * Delete a filter group with the given id. + * + * Requires features{@link Features.filters} or features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#delete} + */ + deleteFilter: async (filterId: string) => { + const response = await client.request( + client.features.filtersV2 ? `/api/v2/filters/${filterId}` : `/api/v1/filters/${filterId}`, + { method: 'DELETE' }, + ); + + return response.json; + }, + + /** + * View keywords added to a filter + * List all keywords attached to the current filter group. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-get} + */ + getFilterKeywords: async (filterId: string) => { + const response = await client.request(`/api/v2/filters/${filterId}/keywords`); + + return v.parse(filteredArray(filterKeywordSchema), response.json); + }, + + /** + * Add a keyword to a filter + * Add the given keyword to the specified filter group + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-create} + */ + addFilterKeyword: async (filterId: string, keyword: string, whole_word?: boolean) => { + const response = await client.request(`/api/v2/filters/${filterId}/keywords`, { + method: 'POST', + body: { keyword, whole_word }, + }); + + return v.parse(filterKeywordSchema, response.json); + }, + + /** + * View a single keyword + * Get one filter keyword by the given id. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-get-one} + */ + getFilterKeyword: async (filterId: string) => { + const response = await client.request(`/api/v2/filters/keywords/${filterId}`); + + return v.parse(filterKeywordSchema, response.json); + }, + + /** + * Edit a keyword within a filter + * Update the given filter keyword. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-update} + */ + updateFilterKeyword: async (filterId: string, keyword: string, whole_word?: boolean) => { + const response = await client.request(`/api/v2/filters/keywords/${filterId}`, { + method: 'PUT', + body: { keyword, whole_word }, + }); + + return v.parse(filterKeywordSchema, response.json); + }, + + /** + * Remove keywords from a filter + * Deletes the given filter keyword. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#keywords-delete} + */ + deleteFilterKeyword: async (filterId: string) => { + const response = await client.request(`/api/v2/filters/keywords/${filterId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * View all status filters + * Obtain a list of all status filters within this filter group. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-get} + */ + getFilterStatuses: async (filterId: string) => { + const response = await client.request(`/api/v2/filters/${filterId}/statuses`); + + return v.parse(filteredArray(filterStatusSchema), response.json); + }, + + /** + * Add a status to a filter group + * Add a status filter to the current filter group. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-add} + */ + addFilterStatus: async (filterId: string, statusId: string) => { + const response = await client.request(`/api/v2/filters/${filterId}/statuses`, { + method: 'POST', + body: { status_id: statusId }, + }); + + return v.parse(filterStatusSchema, response.json); + }, + + /** + * View a single status filter + * Obtain a single status filter. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-get-one} + */ + getFilterStatus: async (statusId: string) => { + const response = await client.request(`/api/v2/filters/statuses/${statusId}`); + + return v.parse(filterStatusSchema, response.json); + }, + + /** + * Remove a status from a filter group + * Remove a status filter from the current filter group. + * + * Requires features{@link Features['filtersV2']}. + * @see {@link https://docs.joinmastodon.org/methods/filters/#statuses-remove} + */ + deleteFilterStatus: async (statusId: string) => { + const response = await client.request(`/api/v2/filters/statuses/${statusId}`, { + method: 'DELETE', + }); + + return response.json; + }, +}); + +export { filtering }; diff --git a/packages/pl-api/lib/client/grouped-notifications.ts b/packages/pl-api/lib/client/grouped-notifications.ts new file mode 100644 index 000000000..af1e4cc1c --- /dev/null +++ b/packages/pl-api/lib/client/grouped-notifications.ts @@ -0,0 +1,242 @@ +import omit from 'lodash.omit'; +import pick from 'lodash.pick'; +import * as v from 'valibot'; + +import { accountSchema, groupedNotificationsResultsSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; +import { type RequestMeta } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + Account, + GroupedNotificationsResults, + Notification, + NotificationGroup, + Status, +} from '../entities'; +import type { + GetGroupedNotificationsParams, + GetUnreadNotificationGroupCountParams, +} from '../params/grouped-notifications'; +import type { PaginatedResponse } from '../responses'; + +type EmptyObject = Record; + +const GROUPED_TYPES = [ + 'favourite', + 'reblog', + 'emoji_reaction', + 'event_reminder', + 'participation_accepted', + 'participation_request', +]; + +const _groupNotifications = ( + { previous, next, items, ...response }: PaginatedResponse, + params?: GetGroupedNotificationsParams, +): PaginatedResponse => { + const notificationGroups: Array = []; + + for (const notification of items) { + let existingGroup: NotificationGroup | undefined; + if ((params?.grouped_types || GROUPED_TYPES).includes(notification.type)) { + existingGroup = notificationGroups.find( + (notificationGroup) => + notificationGroup.type === notification.type && + (notification.type === 'emoji_reaction' && notificationGroup.type === 'emoji_reaction' + ? notification.emoji === notificationGroup.emoji + : true) && + // @ts-expect-error used optional chaining + notificationGroup.status_id === notification.status?.id, + ); + } + + if (existingGroup) { + existingGroup.notifications_count += 1; + existingGroup.page_min_id = notification.id; + existingGroup.sample_account_ids.push(notification.account.id); + } else { + notificationGroups.push({ + ...omit(notification, ['account', 'status', 'target']), + group_key: notification.id, + notifications_count: 1, + most_recent_notification_id: notification.id, + page_min_id: notification.id, + page_max_id: notification.id, + latest_page_notification_at: notification.created_at, + sample_account_ids: [notification.account.id], + // @ts-expect-error used optional chaining + status_id: notification.status?.id, + // @ts-expect-error used optional chaining + target_id: notification.target?.id, + }); + } + } + + const groupedNotificationsResults: GroupedNotificationsResults = { + accounts: Object.values( + items.reduce>((accounts, notification) => { + accounts[notification.account.id] = notification.account; + if ('target' in notification) accounts[notification.target.id] = notification.target; + + return accounts; + }, {}), + ), + statuses: Object.values( + items.reduce>((statuses, notification) => { + if ('status' in notification && notification.status) + statuses[notification.status.id] = notification.status; + return statuses; + }, {}), + ), + notification_groups: notificationGroups, + }; + + return { + ...response, + previous: previous ? async () => _groupNotifications(await previous(), params) : null, + next: next ? async () => _groupNotifications(await next(), params) : null, + items: groupedNotificationsResults, + }; +}; + +/** + * It is recommended to only use this with features{@link Features.groupedNotifications} available. However, there is a fallback that groups the notifications client-side. + */ +const groupedNotifications = ( + client: PlApiBaseClient & { + notifications: ReturnType; + }, +) => { + const category = { + /** + * Get all grouped notifications + * Return grouped notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values. + * + * Requires features{@link Features.groupedNotifications}. + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-grouped} + */ + getGroupedNotifications: async (params: GetGroupedNotificationsParams, meta?: RequestMeta) => { + if (client.features.groupedNotifications) { + return client.paginatedGet( + '/api/v2/notifications', + { ...meta, params }, + groupedNotificationsResultsSchema, + false, + ); + } + + const response = await client.notifications.getNotifications( + pick(params, [ + 'max_id', + 'since_id', + 'limit', + 'min_id', + 'types', + 'exclude_types', + 'account_id', + 'include_filtered', + ]), + ); + + return _groupNotifications(response, params); + }, + + /** + * Get a single notification group + * View information about a specific notification group with a given group key. + * + * Requires features{@link Features.groupedNotifications}. + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-notification-group} + */ + getNotificationGroup: async (groupKey: string) => { + if (client.features.groupedNotifications) { + const response = await client.request(`/api/v2/notifications/${groupKey}`); + + return v.parse(groupedNotificationsResultsSchema, response.json); + } + + const response = await client.request(`/api/v1/notifications/${groupKey}`); + + return _groupNotifications({ + previous: null, + next: null, + items: [response.json], + partial: false, + }).items; + }, + + /** + * Dismiss a single notification group + * Dismiss a single notification group from the server. + * + * Requires features{@link Features.groupedNotifications}. + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group} + */ + dismissNotificationGroup: async (groupKey: string) => { + if (client.features.groupedNotifications) { + const response = await client.request( + `/api/v2/notifications/${groupKey}/dismiss`, + { + method: 'POST', + }, + ); + + return response.json; + } + + return client.notifications.dismissNotification(groupKey); + }, + + /** + * Get accounts of all notifications in a notification group + * + * Requires features{@link Features.groupedNotifications}. + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts} + */ + getNotificationGroupAccounts: async (groupKey: string) => { + if (client.features.groupedNotifications) { + const response = await client.request(`/api/v2/notifications/${groupKey}/accounts`); + + return v.parse(filteredArray(accountSchema), response.json); + } + + return (await category.getNotificationGroup(groupKey)).accounts; + }, + + /** + * Get the number of unread notifications + * Get the (capped) number of unread notification groups for the current user. A notification is considered unread if it is more recent than the notifications read marker. Because the count is dependant on the parameters, it is computed every time and is thus a relatively slow operation (although faster than getting the full corresponding notifications), therefore the number of returned notifications is capped. + * + * Requires features{@link Features.groupedNotifications}. + * @see {@link https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count} + */ + getUnreadNotificationGroupCount: async (params: GetUnreadNotificationGroupCountParams) => { + if (client.features.groupedNotifications) { + const response = await client.request('/api/v2/notifications/unread_count', { params }); + + return v.parse( + v.object({ + count: v.number(), + }), + response.json, + ); + } + + return client.notifications.getUnreadNotificationCount( + pick(params || {}, [ + 'max_id', + 'since_id', + 'limit', + 'min_id', + 'types', + 'exclude_types', + 'account_id', + ]), + ); + }, + }; + return category; +}; + +export { groupedNotifications }; diff --git a/packages/pl-api/lib/client/instance.ts b/packages/pl-api/lib/client/instance.ts new file mode 100644 index 000000000..f83f9332f --- /dev/null +++ b/packages/pl-api/lib/client/instance.ts @@ -0,0 +1,210 @@ +import * as v from 'valibot'; + +import { + accountSchema, + customEmojiSchema, + domainBlockSchema, + extendedDescriptionSchema, + instanceSchema, + privacyPolicySchema, + ruleSchema, + termsOfServiceSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { AKKOMA, MITRA } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { ProfileDirectoryParams } from '../params/instance'; + +const instance = (client: PlApiBaseClient) => ({ + /** + * View server information + * Obtain general information about the server. + * @see {@link https://docs.joinmastodon.org/methods/instance/#v2} + */ + getInstance: async () => { + let response; + try { + response = await client.request('/api/v2/instance'); + } catch (e) { + response = await client.request('/api/v1/instance'); + } + + const instance = v.parse(v.pipe(instanceSchema, v.readonly()), response.json); + client.setInstance(instance); + + return instance; + }, + + /** + * List of connected domains + * Domains that this instance is aware of. + * @see {@link https://docs.joinmastodon.org/methods/instance/#peers} + */ + getInstancePeers: async () => { + const response = await client.request('/api/v1/instance/peers'); + + return v.parse(v.array(v.string()), response.json); + }, + + /** + * Weekly activity + * Instance activity over the last 3 months, binned weekly. + * @see {@link https://docs.joinmastodon.org/methods/instance/#activity} + */ + getInstanceActivity: async () => { + const response = await client.request('/api/v1/instance/activity'); + + return v.parse( + v.array( + v.object({ + week: v.string(), + statuses: v.pipe(v.unknown(), v.transform(String)), + logins: v.pipe(v.unknown(), v.transform(String)), + registrations: v.pipe(v.unknown(), v.transform(String)), + }), + ), + response.json, + ); + }, + + /** + * List of rules + * Rules that the users of this service should follow. + * @see {@link https://docs.joinmastodon.org/methods/instance/#rules} + */ + getInstanceRules: async () => { + const response = await client.request('/api/v1/instance/rules'); + + return v.parse(filteredArray(ruleSchema), response.json); + }, + + /** + * View moderated servers + * Obtain a list of domains that have been blocked. + * @see {@link https://docs.joinmastodon.org/methods/instance/#domain_blocks} + */ + getInstanceDomainBlocks: async () => { + const response = await client.request('/api/v1/instance/rules'); + + return v.parse(filteredArray(domainBlockSchema), response.json); + }, + + /** + * View extended description + * Obtain an extended description of this server + * @see {@link https://docs.joinmastodon.org/methods/instance/#extended_description} + */ + getInstanceExtendedDescription: async () => { + const response = await client.request('/api/v1/instance/extended_description'); + + return v.parse(extendedDescriptionSchema, response.json); + }, + + /** + * View translation languages + * Translation language pairs supported by the translation engine used by the server. + * @see {@link https://docs.joinmastodon.org/methods/instance/#translation_languages} + */ + getInstanceTranslationLanguages: async () => { + if (client.features.version.software === AKKOMA) { + const response = await client.request<{ + source: Array<{ code: string; name: string }>; + target: Array<{ code: string; name: string }>; + }>('/api/v1/akkoma/translation/languages'); + + return Object.fromEntries( + response.json.source.map((source) => [ + source.code.toLocaleLowerCase(), + response.json.target + .map((lang) => lang.code) + .filter((lang) => lang !== source.code) + .map((lang) => lang.toLocaleLowerCase()), + ]), + ); + } + + const response = await client.request('/api/v1/instance/translation_languages'); + + return v.parse(v.record(v.string(), v.array(v.string())), response.json); + }, + + /** + * View profile directory + * List accounts visible in the directory. + * @see {@link https://docs.joinmastodon.org/methods/directory/#get} + * + * Requires features{@link Features.profileDirectory}. + */ + profileDirectory: async (params?: ProfileDirectoryParams) => { + const response = await client.request('/api/v1/directory', { params }); + + return v.parse(filteredArray(accountSchema), response.json); + }, + + /** + * View all custom emoji + * Returns custom emojis that are available on the server. + * @see {@link https://docs.joinmastodon.org/methods/custom_emojis/#get} + */ + getCustomEmojis: async () => { + const response = await client.request('/api/v1/custom_emojis'); + + return v.parse(filteredArray(customEmojiSchema), response.json); + }, + + /** + * Dump frontend configurations + * + * Requires features{@link Features.frontendConfigurations}. + */ + getFrontendConfigurations: async () => { + let response; + + switch (client.features.version.software) { + case MITRA: + response = (await client.request('/api/v1/accounts/verify_credentials')).json + ?.client_config; + break; + default: + response = (await client.request('/api/pleroma/frontend_configurations')).json; + } + + return v.parse(v.fallback(v.record(v.string(), v.record(v.string(), v.any())), {}), response); + }, + + /** + * View privacy policy + * Obtain the contents of this server's privacy policy. + * @see {@link https://docs.joinmastodon.org/methods/instance/privacy_policy} + */ + getInstancePrivacyPolicy: async () => { + const response = await client.request('/api/v1/instance/privacy_policy'); + + return v.parse(privacyPolicySchema, response.json); + }, + + /** + * View terms of service + * Obtain the contents of this server's terms of service, if configured. + * @see {@link https://docs.joinmastodon.org/methods/instance/terms_of_service} + */ + getInstanceTermsOfService: async () => { + const response = await client.request('/api/v1/instance/terms_of_service'); + + return v.parse(termsOfServiceSchema, response.json); + }, + + /** + * View a specific version of the terms of service + * Obtain the contents of this server's terms of service, for a specified date, if configured. + * @see {@link https://docs.joinmastodon.org/methods/instance/terms_of_service_date} + */ + getInstanceTermsOfServiceForDate: async (date: string) => { + const response = await client.request(`/api/v1/instance/terms_of_service/${date}`); + + return v.parse(termsOfServiceSchema, response.json); + }, +}); + +export { instance }; diff --git a/packages/pl-api/lib/client/interaction-requests.ts b/packages/pl-api/lib/client/interaction-requests.ts new file mode 100644 index 000000000..745900ce7 --- /dev/null +++ b/packages/pl-api/lib/client/interaction-requests.ts @@ -0,0 +1,57 @@ +import * as v from 'valibot'; + +import { interactionRequestSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { GetInteractionRequestsParams } from '../params/interaction-requests'; + +const interactionRequests = (client: PlApiBaseClient) => ({ + /** + * Get an array of interactions requested on your statuses by other accounts, and pending your approval. + * + * Requires features{@link Features.interactionRequests}. + */ + getInteractionRequests: (params?: GetInteractionRequestsParams) => + client.paginatedGet('/api/v1/interaction_requests', { params }, interactionRequestSchema), + + /** + * Get interaction request with the given ID. + * + * Requires features{@link Features.interactionRequests}. + */ + getInteractionRequest: async (interactionRequestId: string) => { + const response = await client.request(`/api/v1/interaction_requests/${interactionRequestId}`); + + return v.parse(interactionRequestSchema, response.json); + }, + + /** + * Accept/authorize/approve an interaction request with the given ID. + * + * Requires features{@link Features.interactionRequests}. + */ + authorizeInteractionRequest: async (interactionRequestId: string) => { + const response = await client.request( + `/api/v1/interaction_requests/${interactionRequestId}/authorize`, + { method: 'POST' }, + ); + + return v.parse(interactionRequestSchema, response.json); + }, + + /** + * Reject an interaction request with the given ID. + * + * Requires features{@link Features.interactionRequests}. + */ + rejectInteractionRequest: async (interactionRequestId: string) => { + const response = await client.request( + `/api/v1/interaction_requests/${interactionRequestId}/authorize`, + { method: 'POST' }, + ); + + return v.parse(interactionRequestSchema, response.json); + }, +}); + +export { interactionRequests }; diff --git a/packages/pl-api/lib/client/lists.ts b/packages/pl-api/lib/client/lists.ts new file mode 100644 index 000000000..659df9647 --- /dev/null +++ b/packages/pl-api/lib/client/lists.ts @@ -0,0 +1,131 @@ +import * as v from 'valibot'; + +import { accountSchema, listSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; +import type { CreateListParams, GetListAccountsParams, UpdateListParams } from '../params/lists'; + +type EmptyObject = Record; + +const lists = (client: PlApiBaseClient) => ({ + /** + * View your lists + * Fetch all lists that the user owns. + * @see {@link https://docs.joinmastodon.org/methods/lists/#get} + */ + getLists: async () => { + const response = await client.request('/api/v1/lists'); + + return v.parse(filteredArray(listSchema), response.json); + }, + + /** + * Show a single list + * Fetch the list with the given ID. Used for verifying the title of a list, and which replies to show within that list. + * @see {@link https://docs.joinmastodon.org/methods/lists/#get-one} + */ + getList: async (listId: string) => { + const response = await client.request(`/api/v1/lists/${listId}`); + + return v.parse(listSchema, response.json); + }, + + /** + * Create a list + * Create a new list. + * @see {@link https://docs.joinmastodon.org/methods/lists/#create} + */ + createList: async (params: CreateListParams) => { + const response = await client.request('/api/v1/lists', { method: 'POST', body: params }); + + return v.parse(listSchema, response.json); + }, + + /** + * Update a list + * Change the title of a list, or which replies to show. + * @see {@link https://docs.joinmastodon.org/methods/lists/#update} + */ + updateList: async (listId: string, params: UpdateListParams) => { + const response = await client.request(`/api/v1/lists/${listId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(listSchema, response.json); + }, + + /** + * Delete a list + * @see {@link https://docs.joinmastodon.org/methods/lists/#delete} + */ + deleteList: async (listId: string) => { + const response = await client.request(`/api/v1/lists/${listId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * View accounts in a list + * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts} + */ + getListAccounts: (listId: string, params?: GetListAccountsParams) => + client.paginatedGet(`/api/v1/lists/${listId}/accounts`, { params }, accountSchema), + + /** + * Add accounts to a list + * Add accounts to the given list. Note that the user must be following these accounts. + * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts-add} + */ + addListAccounts: async (listId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/lists/${listId}/accounts`, { + method: 'POST', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** + * Remove accounts from list + * Remove accounts from the given list. + * @see {@link https://docs.joinmastodon.org/methods/lists/#accounts-remove} + */ + deleteListAccounts: async (listId: string, accountIds: string[]) => { + const response = await client.request(`/api/v1/lists/${listId}/accounts`, { + method: 'DELETE', + body: { account_ids: accountIds }, + }); + + return response.json; + }, + + /** + * Add a list to favourites + * + * Requires features{@link Features.listsFavourite}. + */ + favouriteList: async (listId: string) => { + const response = await client.request(`/api/v1/lists/${listId}/favourite`, { method: 'POST' }); + + return v.parse(listSchema, response.json); + }, + + /** + * Remove a list from favourites + * + * Requires features{@link Features.listsFavourite}. + */ + unfavouriteList: async (listId: string) => { + const response = await client.request(`/api/v1/lists/${listId}/unfavourite`, { + method: 'POST', + }); + + return v.parse(listSchema, response.json); + }, +}); + +export { lists }; diff --git a/packages/pl-api/lib/client/media.ts b/packages/pl-api/lib/client/media.ts new file mode 100644 index 000000000..40e2da133 --- /dev/null +++ b/packages/pl-api/lib/client/media.ts @@ -0,0 +1,68 @@ +import * as v from 'valibot'; + +import { mediaAttachmentSchema } from '../entities'; +import { type RequestMeta } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { UpdateMediaParams, UploadMediaParams } from '../params/media'; + +type EmptyObject = Record; + +const media = (client: PlApiBaseClient) => ({ + /** + * Upload media as an attachment + * Creates a media attachment to be used with a new status. The full sized media will be processed asynchronously in the background for large uploads. + * @see {@link https://docs.joinmastodon.org/methods/media/#v2} + */ + uploadMedia: async (params: UploadMediaParams, meta?: RequestMeta) => { + const response = await client.request( + client.features.mediaV2 ? '/api/v2/media' : '/api/v1/media', + { ...meta, method: 'POST', body: params, contentType: '' }, + ); + + return v.parse(mediaAttachmentSchema, response.json); + }, + + /** + * Get media attachment + * Get a media attachment, before it is attached to a status and posted, but after it is accepted for processing. Use this method to check that the full-sized media has finished processing. + * @see {@link https://docs.joinmastodon.org/methods/media/#get} + */ + getMedia: async (attachmentId: string) => { + const response = await client.request(`/api/v1/media/${attachmentId}`); + + return v.parse(mediaAttachmentSchema, response.json); + }, + + /** + * Update media attachment + * Update a MediaAttachment’s parameters, before it is attached to a status and posted. + * @see {@link https://docs.joinmastodon.org/methods/media/#update} + */ + updateMedia: async (attachmentId: string, params: UpdateMediaParams) => { + const response = await client.request(`/api/v1/media/${attachmentId}`, { + method: 'PUT', + body: params, + contentType: params.thumbnail ? '' : undefined, + }); + + return v.parse(mediaAttachmentSchema, response.json); + }, + + /** + * Update media attachment + * Update a MediaAttachment’s parameters, before it is attached to a status and posted. + * + * Requires features{@link Features.deleteMedia}. + * @see {@link https://docs.joinmastodon.org/methods/media/delete} + */ + deleteMedia: async (attachmentId: string) => { + const response = await client.request(`/api/v1/media/${attachmentId}`, { + method: 'DELETE', + }); + + return response.json; + }, +}); + +export { media }; diff --git a/packages/pl-api/lib/client/my-account.ts b/packages/pl-api/lib/client/my-account.ts new file mode 100644 index 000000000..861179afa --- /dev/null +++ b/packages/pl-api/lib/client/my-account.ts @@ -0,0 +1,371 @@ +import * as v from 'valibot'; + +import { + accountSchema, + bookmarkFolderSchema, + featuredTagSchema, + relationshipSchema, + statusSchema, + suggestionSchema, + tagSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { GOTOSOCIAL, ICESHRIMP_NET, PIXELFED, PLEROMA } from '../features'; +import { getNextLink, getPrevLink } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { Account } from '../entities'; +import type { + CreateBookmarkFolderParams, + GetBookmarksParams, + GetEndorsementsParams, + GetFavouritesParams, + GetFollowRequestsParams, + GetFollowedTagsParams, + UpdateBookmarkFolderParams, +} from '../params/my-account'; +import type { PaginatedResponse } from '../responses'; + +type EmptyObject = Record; + +const paginatedIceshrimpAccountsList = async ( + client: PlApiBaseClient & { accounts: ReturnType }, + url: string, + fn: (body: T) => Array, +): Promise> => { + await client.getIceshrimpAccessToken(); + + const response = await client.request(url); + const ids = fn(response.json); + + const items = await client.accounts.getAccounts(ids); + + const prevLink = getPrevLink(response); + const nextLink = getNextLink(response); + + return { + previous: prevLink ? () => paginatedIceshrimpAccountsList(client, prevLink, fn) : null, + next: nextLink ? () => paginatedIceshrimpAccountsList(client, nextLink, fn) : null, + items, + partial: response.status === 206, + }; +}; + +const myAccount = ( + client: PlApiBaseClient & { accounts: ReturnType }, +) => ({ + /** + * View bookmarked statuses + * Statuses the user has bookmarked. + * @see {@link https://docs.joinmastodon.org/methods/bookmarks/#get} + */ + getBookmarks: (params?: GetBookmarksParams) => + client.paginatedGet( + client.features.bookmarkFoldersMultiple && params?.folder_id + ? `/api/v1/bookmark_categories/${params.folder_id}/statuses` + : '/api/v1/bookmarks', + { params }, + statusSchema, + ), + + /** + * View favourited statuses + * Statuses the user has favourited. + * @see {@link https://docs.joinmastodon.org/methods/favourites/#get} + */ + getFavourites: (params?: GetFavouritesParams) => + client.paginatedGet('/api/v1/favourites', { params }, statusSchema), + + /** + * View pending follow requests + * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#get} + */ + getFollowRequests: (params?: GetFollowRequestsParams) => + client.paginatedGet('/api/v1/follow_requests', { params }, accountSchema), + + /** + * View outgoing follow requests + * + * Requires features{@link Features.outgoingFollowRequests}. + */ + getOutgoingFollowRequests: (params?: GetFollowRequestsParams) => { + if (client.features.version.software === ICESHRIMP_NET) { + return paginatedIceshrimpAccountsList( + client, + '/api/iceshrimp/follow_requests/outgoing', + (response: Array<{ user: { id: string } }>) => response.map(({ user }) => user.id), + ); + } + + switch (client.features.version.software) { + case GOTOSOCIAL: + return client.paginatedGet('/api/v1/follow_requests/outgoing', { params }, accountSchema); + + default: + return client.paginatedGet( + '/api/v1/pleroma/outgoing_follow_requests', + { params }, + accountSchema, + ); + } + }, + + /** + * Accept follow request + * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#accept} + */ + acceptFollowRequest: async (accountId: string) => { + const response = await client.request(`/api/v1/follow_requests/${accountId}/authorize`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * Reject follow request + * @see {@link https://docs.joinmastodon.org/methods/follow_requests/#reject} + */ + rejectFollowRequest: async (accountId: string) => { + const response = await client.request(`/api/v1/follow_requests/${accountId}/reject`, { + method: 'POST', + }); + + return v.parse(relationshipSchema, response.json); + }, + + /** + * View currently featured profiles + * Accounts that the user is currently featuring on their profile. + * @see {@link https://docs.joinmastodon.org/methods/endorsements/#get} + */ + getEndorsements: (params?: GetEndorsementsParams) => + client.paginatedGet('/api/v1/endorsements', { params }, accountSchema), + + /** + * View your featured tags + * List all hashtags featured on your profile. + * + * Requires features{@link Features.featuredTags}. + * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#get} + */ + getFeaturedTags: async () => { + const response = await client.request('/api/v1/featured_tags'); + + return v.parse(filteredArray(featuredTagSchema), response.json); + }, + + /** + * Feature a tag + * Promote a hashtag on your profile. + * + * Requires features{@link Features.featuredTags}. + * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#feature} + */ + featureTag: async (name: string) => { + const response = await client.request('/api/v1/featured_tags', { + method: 'POST', + body: { name }, + }); + + return v.parse(filteredArray(featuredTagSchema), response.json); + }, + + /** + * Unfeature a tag + * Stop promoting a hashtag on your profile. + * + * Requires features{@link Features.featuredTags}. + * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#unfeature} + */ + unfeatureTag: async (name: string) => { + const response = await client.request('/api/v1/featured_tags', { + method: 'DELETE', + body: { name }, + }); + + return response.json; + }, + + /** + * View suggested tags to feature + * Shows up to 10 recently-used tags. + * + * Requires features{@link Features.featuredTags}. + * @see {@link https://docs.joinmastodon.org/methods/featured_tags/#suggestions} + */ + getFeaturedTagsSuggestions: async () => { + const response = await client.request('/api/v1/featured_tags/suggestions'); + + return v.parse(filteredArray(tagSchema), response.json); + }, + + /** + * View all followed tags + * List your followed hashtags. + * + * Requires features{@link Features.followHashtags}. + * @see {@link https://docs.joinmastodon.org/methods/followed_tags/#get} + */ + getFollowedTags: (params?: GetFollowedTagsParams) => + client.paginatedGet('/api/v1/followed_tags', { params }, tagSchema), + + /** + * View information about a single tag + * Show a hashtag and its associated information + * @see {@link https://docs.joinmastodon.org/methods/tags/#get} + */ + getTag: async (tagId: string) => { + const response = await client.request(`/api/v1/tags/${tagId}`); + + return v.parse(tagSchema, response.json); + }, + + /** + * Follow a hashtag + * Follow a hashtag. Posts containing a followed hashtag will be inserted into your home timeline. + * @see {@link https://docs.joinmastodon.org/methods/tags/#follow} + */ + followTag: async (tagId: string) => { + const response = await client.request(`/api/v1/tags/${tagId}/follow`, { method: 'POST' }); + + return v.parse(tagSchema, response.json); + }, + + /** + * Unfollow a hashtag + * Unfollow a hashtag. Posts containing this hashtag will no longer be inserted into your home timeline. + * @see {@link https://docs.joinmastodon.org/methods/tags/#unfollow} + */ + unfollowTag: async (tagId: string) => { + const response = await client.request(`/api/v1/tags/${tagId}/unfollow`, { method: 'POST' }); + + return v.parse(tagSchema, response.json); + }, + + /** + * View follow suggestions + * Accounts that are promoted by staff, or that the user has had past positive interactions with, but is not yet following. + * + * Requires features{@link Features.suggestions}. + * @see {@link https://docs.joinmastodon.org/methods/suggestions/#v2} + */ + getSuggestions: async (limit?: number) => { + const response = await client.request( + client.features.version.software === PIXELFED + ? '/api/v1.1/discover/accounts/popular' + : client.features.suggestionsV2 + ? '/api/v2/suggestions' + : '/api/v1/suggestions', + { params: { limit } }, + ); + + return v.parse(filteredArray(suggestionSchema), response.json); + }, + + /** + * Remove a suggestion + * Remove an account from follow suggestions. + * + * Requires features{@link Features.suggestionsDismiss}. + * @see {@link https://docs.joinmastodon.org/methods/suggestions/#remove} + */ + dismissSuggestions: async (accountId: string) => { + const response = await client.request(`/api/v1/suggestions/${accountId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * Gets user bookmark folders + * + * Requires features{@link Features.bookmarkFolders}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromabookmark_folders} + */ + getBookmarkFolders: async () => { + const response = await client.request( + client.features.version.software === PLEROMA + ? '/api/v1/pleroma/bookmark_folders' + : '/api/v1/bookmark_categories', + ); + + return v.parse(filteredArray(bookmarkFolderSchema), response.json); + }, + + /** + * Creates a bookmark folder + * + * Requires features{@link Features.bookmarkFolders}. + * Specifying folder emoji requires features{@link Features.bookmarkFolderEmojis}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#post-apiv1pleromabookmark_folders} + */ + createBookmarkFolder: async (params: CreateBookmarkFolderParams) => { + const response = await client.request( + client.features.version.software === PLEROMA + ? '/api/v1/pleroma/bookmark_folders' + : '/api/v1/bookmark_categories', + { method: 'POST', body: { title: params.name, ...params } }, + ); + + return v.parse(bookmarkFolderSchema, response.json); + }, + + /** + * Updates a bookmark folder + * + * Requires features{@link Features.bookmarkFolders}. + * Specifying folder emoji requires features{@link Features.bookmarkFolderEmojis}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#patch-apiv1pleromabookmark_foldersid} + */ + updateBookmarkFolder: async (bookmarkFolderId: string, params: UpdateBookmarkFolderParams) => { + const response = await client.request( + `${client.features.version.software === PLEROMA ? '/api/v1/pleroma/bookmark_folders' : '/api/v1/bookmark_categories'}/${bookmarkFolderId}`, + { method: 'PATCH', body: { title: params.name, ...params } }, + ); + + return v.parse(bookmarkFolderSchema, response.json); + }, + + /** + * Deletes a bookmark folder + * + * Requires features{@link Features.bookmarkFolders}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromabookmark_foldersid} + */ + deleteBookmarkFolder: async (bookmarkFolderId: string) => { + const response = await client.request( + `${client.features.version.software === PLEROMA ? '/api/v1/pleroma/bookmark_folders' : '/api/v1/bookmark_categories'}/${bookmarkFolderId}`, + { method: 'DELETE' }, + ); + + return v.parse(bookmarkFolderSchema, response.json); + }, + + /** + * Requires features{@link Features.bookmarkFoldersMultiple}. + */ + addBookmarkToFolder: async (statusId: string, folderId: string) => { + const response = await client.request( + `/api/v1/bookmark_categories/${folderId}/statuses`, + { method: 'POST', params: { status_ids: [statusId] } }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.bookmarkFoldersMultiple}. + */ + removeBookmarkFromFolder: async (statusId: string, folderId: string) => { + const response = await client.request( + `/api/v1/bookmark_categories/${folderId}/statuses`, + { method: 'DELETE', params: { status_ids: [statusId] } }, + ); + + return response.json; + }, +}); + +export { myAccount }; diff --git a/packages/pl-api/lib/client/notifications.ts b/packages/pl-api/lib/client/notifications.ts new file mode 100644 index 000000000..e511f7380 --- /dev/null +++ b/packages/pl-api/lib/client/notifications.ts @@ -0,0 +1,255 @@ +import * as v from 'valibot'; + +import { + notificationPolicySchema, + notificationRequestSchema, + notificationSchema, +} from '../entities'; +import { type RequestMeta } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + GetNotificationParams, + GetNotificationRequestsParams, + GetUnreadNotificationCountParams, + UpdateNotificationPolicyRequest, +} from '../params/notifications'; + +type EmptyObject = Record; + +const notifications = (client: PlApiBaseClient) => ({ + /** + * Get all notifications + * Notifications concerning the user. This API returns Link headers containing links to the next/previous page. However, the links can also be constructed dynamically using query params and `id` values. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#get} + */ + getNotifications: (params?: GetNotificationParams, meta?: RequestMeta) => { + const PLEROMA_TYPES = [ + 'chat_mention', + 'emoji_reaction', + 'report', + 'participation_accepted', + 'participation_request', + 'event_reminder', + 'event_update', + ]; + + if (params?.types) + params.types = [ + ...params.types, + ...params.types + .filter((type) => PLEROMA_TYPES.includes(type)) + .map((type) => `pleroma:${type}`), + ]; + + if (params?.exclude_types) + params.exclude_types = [ + ...params.exclude_types, + ...params.exclude_types + .filter((type) => PLEROMA_TYPES.includes(type)) + .map((type) => `pleroma:${type}`), + ]; + + return client.paginatedGet('/api/v1/notifications', { ...meta, params }, notificationSchema); + }, + + /** + * Get a single notification + * View information about a notification with a given ID. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-one} + */ + getNotification: async (notificationId: string) => { + const response = await client.request(`/api/v1/notifications/${notificationId}`); + + return v.parse(notificationSchema, response.json); + }, + + /** + * Dismiss all notifications + * Clear all notifications from the server. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#clear} + */ + dismissNotifications: async () => { + const response = await client.request('/api/v1/notifications/clear', { + method: 'POST', + }); + + return response.json; + }, + + /** + * Dismiss a single notification + * Dismiss a single notification from the server. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss} + */ + dismissNotification: async (notificationId: string) => { + const response = await client.request( + `/api/v1/notifications/${notificationId}/dismiss`, + { + method: 'POST', + }, + ); + + return response.json; + }, + + /** + * Get the number of unread notification + * Get the (capped) number of unread notifications for the current user. + * + * Requires features{@link Features.notificationsGetUnreadCount}. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#unread-count} + */ + getUnreadNotificationCount: async (params?: GetUnreadNotificationCountParams) => { + const response = await client.request('/api/v1/notifications/unread_count', { params }); + + return v.parse( + v.object({ + count: v.number(), + }), + response.json, + ); + }, + + /** + * Get the filtering policy for notifications + * Notifications filtering policy for the user. + * + * Requires features{@link Features.notificationsPolicy}. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-policy} + */ + getNotificationPolicy: async () => { + const response = await client.request('/api/v2/notifications/policy'); + + return v.parse(notificationPolicySchema, response.json); + }, + + /** + * Update the filtering policy for notifications + * Update the user’s notifications filtering policy. + * + * Requires features{@link Features.notificationsPolicy}. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#update-the-filtering-policy-for-notifications} + */ + updateNotificationPolicy: async (params: UpdateNotificationPolicyRequest) => { + const response = await client.request('/api/v2/notifications/policy', { + method: 'PATCH', + body: params, + }); + + return v.parse(notificationPolicySchema, response.json); + }, + + /** + * Get all notification requests + * Notification requests for notifications filtered by the user’s policy. This API returns Link headers containing links to the next/previous page. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-requests} + */ + getNotificationRequests: (params?: GetNotificationRequestsParams) => + client.paginatedGet('/api/v1/notifications/requests', { params }, notificationRequestSchema), + + /** + * Get a single notification request + * View information about a notification request with a given ID. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#get-one-request} + */ + getNotificationRequest: async (notificationRequestId: string) => { + const response = await client.request( + `/api/v1/notifications/requests/${notificationRequestId}`, + ); + + return v.parse(notificationRequestSchema, response.json); + }, + + /** + * Accept a single notification request + * Accept a notification request, which merges the filtered notifications from that user back into the main notification and accepts any future notification from them. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#accept-request} + */ + acceptNotificationRequest: async (notificationRequestId: string) => { + const response = await client.request( + `/api/v1/notifications/requests/${notificationRequestId}/dismiss`, + { method: 'POST' }, + ); + + return response.json; + }, + + /** + * Dismiss a single notification request + * Dismiss a notification request, which hides it and prevent it from contributing to the pending notification requests count. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss-request} + */ + dismissNotificationRequest: async (notificationRequestId: string) => { + const response = await client.request( + `/api/v1/notifications/requests/${notificationRequestId}/dismiss`, + { method: 'POST' }, + ); + + return response.json; + }, + + /** + * Accept multiple notification requests + * Accepts multiple notification requests, which merges the filtered notifications from those users back into the main notifications and accepts any future notification from them. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#accept-multiple-requests} + * Requires features{@link Features.notificationsRequestsAcceptMultiple}. + */ + acceptMultipleNotificationRequests: async (notificationRequestIds: Array) => { + const response = await client.request('/api/v1/notifications/requests/accept', { + method: 'POST', + body: { id: notificationRequestIds }, + }); + + return response.json; + }, + + /** + * Dismiss multiple notification requests + * Dismiss multiple notification requests, which hides them and prevent them from contributing to the pending notification requests count. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#dismiss-multiple-requests} + * Requires features{@link Features.notificationsRequestsAcceptMultiple}. + */ + dismissMultipleNotificationRequests: async (notificationRequestIds: Array) => { + const response = await client.request('/api/v1/notifications/requests/dismiss', { + method: 'POST', + body: { id: notificationRequestIds }, + }); + + return response.json; + }, + + /** + * Check if accepted notification requests have been merged + * Check whether accepted notification requests have been merged. Accepting notification requests schedules a background job to merge the filtered notifications back into the normal notification list. When that process has finished, the client should refresh the notifications list at its earliest convenience. This is communicated by the `notifications_merged` streaming event but can also be polled using this endpoint. + * @see {@link https://docs.joinmastodon.org/methods/notifications/#requests-merged} + * Requires features{@link Features.notificationsRequestsAcceptMultiple}. + */ + checkNotificationRequestsMerged: async () => { + const response = await client.request('/api/v1/notifications/requests/merged'); + + return v.parse( + v.object({ + merged: v.boolean(), + }), + response.json, + ); + }, + + /** + * An endpoint to delete multiple statuses by IDs. + * + * Requires features{@link Features.notificationsDismissMultiple}. + * @see {@link https://docs.pleroma.social/backend/development/API/differences_in_mastoapi_responses/#delete-apiv1notificationsdestroy_multiple} + */ + dismissMultipleNotifications: async (notificationIds: string[]) => { + const response = await client.request('/api/v1/notifications/destroy_multiple', { + params: { ids: notificationIds }, + method: 'DELETE', + }); + + return response.json; + }, +}); + +export { notifications }; diff --git a/packages/pl-api/lib/client/oauth.ts b/packages/pl-api/lib/client/oauth.ts new file mode 100644 index 000000000..4f998c1e3 --- /dev/null +++ b/packages/pl-api/lib/client/oauth.ts @@ -0,0 +1,145 @@ +import * as v from 'valibot'; + +import { authorizationServerMetadataSchema, tokenSchema, userInfoSchema } from '../entities'; +import { ICESHRIMP_NET } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + GetTokenParams, + MfaChallengeParams, + OauthAuthorizeParams, + RevokeTokenParams, +} from '../params/oauth'; + +type EmptyObject = Record; + +const oauth = (client: PlApiBaseClient) => ({ + /** + * Authorize a user + * Displays an authorization form to the user. If approved, it will create and return an authorization code, then redirect to the desired `redirect_uri`, or show the authorization code if `urn:ietf:wg:oauth:2.0:oob` was requested. The authorization code can be used while requesting a token to obtain access to user-level methods. + * @see {@link https://docs.joinmastodon.org/methods/oauth/#authorize} + */ + authorize: async (params: OauthAuthorizeParams) => { + const response = await client.request('/oauth/authorize', { params, contentType: '' }); + + return v.parse(v.string(), response.json); + }, + + /** + * Obtain a token + * Obtain an access token, to be used during API calls that are not public. + * @see {@link https://docs.joinmastodon.org/methods/oauth/#token} + */ + getToken: async (params: GetTokenParams) => { + if (client.features.version.software === ICESHRIMP_NET && params.grant_type === 'password') { + const loginResponse = ( + await client.request<{ + token: string; + }>('/api/iceshrimp/auth/login', { + method: 'POST', + body: { + username: params.username, + password: params.password, + }, + }) + ).json; + client.setIceshrimpAccessToken(loginResponse.token); + + const mastodonTokenResponse = ( + await client.request<{ + id: string; + token: string; + created_at: string; + scopes: Array; + }>('/api/iceshrimp/sessions/mastodon', { + method: 'POST', + body: { + appName: params.client_id, + scopes: params.scope?.split(' '), + flags: { + supportsHtmlFormatting: true, + autoDetectQuotes: false, + isPleroma: true, + supportsInlineMedia: true, + }, + }, + }) + ).json; + + return v.parse(tokenSchema, { + access_token: mastodonTokenResponse.token, + token_type: 'Bearer', + scope: mastodonTokenResponse.scopes.join(' '), + created_at: new Date(mastodonTokenResponse.created_at).getTime(), + id: mastodonTokenResponse.id, + }); + } + const response = await client.request('/oauth/token', { + method: 'POST', + body: params, + contentType: '', + }); + + return v.parse(tokenSchema, { scope: params.scope || '', ...response.json }); + }, + + /** + * Revoke a token + * Revoke an access token to make it no longer valid for use. + * @see {@link https://docs.joinmastodon.org/methods/oauth/#revoke} + */ + revokeToken: async (params: RevokeTokenParams) => { + const response = await client.request('/oauth/revoke', { + method: 'POST', + body: params, + contentType: '', + }); + + client.socket?.close(); + + return response.json; + }, + + /** + * Retrieve user information + * Retrieves standardised OIDC claims about the currently authenticated user. + * see {@link https://docs.joinmastodon.org/methods/oauth/#userinfo} + */ + userinfo: async () => { + const response = await client.request('/oauth/userinfo'); + + return v.parse(userInfoSchema, response.json); + }, + + authorizationServerMetadata: async () => { + const response = await client.request('/.well-known/oauth-authorization-server'); + + return v.parse(authorizationServerMetadataSchema, response.json); + }, + + /** + * Get a new captcha + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apiv1pleromacaptcha} + */ + getCaptcha: async () => { + const response = await client.request('/api/pleroma/captcha'); + + return v.parse( + v.intersect([ + v.object({ + type: v.string(), + }), + v.record(v.string(), v.any()), + ]), + response.json, + ); + }, + + mfaChallenge: async (params: MfaChallengeParams) => { + const response = await client.request('/oauth/mfa/challenge', { method: 'POST', body: params }); + + return v.parse(tokenSchema, response.json); + }, +}); + +export { oauth }; diff --git a/packages/pl-api/lib/client/oembed.ts b/packages/pl-api/lib/client/oembed.ts new file mode 100644 index 000000000..feb73acb5 --- /dev/null +++ b/packages/pl-api/lib/client/oembed.ts @@ -0,0 +1,31 @@ +import * as v from 'valibot'; + +import type { PlApiBaseClient } from '../client-base'; + +const oembed = (client: PlApiBaseClient) => ({ + /** + * Get OEmbed info as JSON + * @see {@link https://docs.joinmastodon.org/methods/oembed/#get} + */ + getOembed: async (url: string, maxwidth?: number, maxheight?: number) => { + const response = await client.request('/api/oembed', { params: { url, maxwidth, maxheight } }); + + return v.parse( + v.object({ + type: v.fallback(v.string(), 'rich'), + version: v.fallback(v.string(), ''), + author_name: v.fallback(v.string(), ''), + author_url: v.fallback(v.string(), ''), + provider_name: v.fallback(v.string(), ''), + provider_url: v.fallback(v.string(), ''), + cache_age: v.number(), + html: v.string(), + width: v.fallback(v.nullable(v.number()), null), + height: v.fallback(v.nullable(v.number()), null), + }), + response.json, + ); + }, +}); + +export { oembed }; diff --git a/packages/pl-api/lib/client/polls.ts b/packages/pl-api/lib/client/polls.ts new file mode 100644 index 000000000..8f16b4e07 --- /dev/null +++ b/packages/pl-api/lib/client/polls.ts @@ -0,0 +1,34 @@ +import * as v from 'valibot'; + +import { pollSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; + +const polls = (client: PlApiBaseClient) => ({ + /** + * View a poll + * View a poll attached to a status. + * @see {@link https://docs.joinmastodon.org/methods/polls/#get} + */ + getPoll: async (pollId: string) => { + const response = await client.request(`/api/v1/polls/${pollId}`); + + return v.parse(pollSchema, response.json); + }, + + /** + * Vote on a poll + * Vote on a poll attached to a status. + * @see {@link https://docs.joinmastodon.org/methods/polls/#vote} + */ + vote: async (pollId: string, choices: number[]) => { + const response = await client.request(`/api/v1/polls/${pollId}/votes`, { + method: 'POST', + body: { choices }, + }); + + return v.parse(pollSchema, response.json); + }, +}); + +export { polls }; diff --git a/packages/pl-api/lib/client/push-notifications.ts b/packages/pl-api/lib/client/push-notifications.ts new file mode 100644 index 000000000..4d0f24c37 --- /dev/null +++ b/packages/pl-api/lib/client/push-notifications.ts @@ -0,0 +1,67 @@ +import * as v from 'valibot'; + +import { webPushSubscriptionSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreatePushNotificationsSubscriptionParams, + UpdatePushNotificationsSubscriptionParams, +} from '../params/push-notifications'; + +type EmptyObject = Record; + +const pushNotifications = (client: PlApiBaseClient) => ({ + /** + * Subscribe to push notifications + * Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted. + * @see {@link https://docs.joinmastodon.org/methods/push/#create} + */ + createSubscription: async (params: CreatePushNotificationsSubscriptionParams) => { + const response = await client.request('/api/v1/push/subscription', { + method: 'POST', + body: params, + }); + + return v.parse(webPushSubscriptionSchema, response.json); + }, + + /** + * Get current subscription + * View the PushSubscription currently associated with this access token. + * @see {@link https://docs.joinmastodon.org/methods/push/#get} + */ + getSubscription: async () => { + const response = await client.request('/api/v1/push/subscription'); + + return v.parse(webPushSubscriptionSchema, response.json); + }, + + /** + * Change types of notifications + * Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead. + * @see {@link https://docs.joinmastodon.org/methods/push/#update} + */ + updateSubscription: async (params: UpdatePushNotificationsSubscriptionParams) => { + const response = await client.request('/api/v1/push/subscription', { + method: 'PUT', + body: params, + }); + + return v.parse(webPushSubscriptionSchema, response.json); + }, + + /** + * Remove current subscription + * Removes the current Web Push API subscription. + * @see {@link https://docs.joinmastodon.org/methods/push/#delete} + */ + deleteSubscription: async () => { + const response = await client.request('/api/v1/push/subscription', { + method: 'DELETE', + }); + + return response.json; + }, +}); + +export { pushNotifications }; diff --git a/packages/pl-api/lib/client/rss-feed-subscriptions.ts b/packages/pl-api/lib/client/rss-feed-subscriptions.ts new file mode 100644 index 000000000..9ff1754c1 --- /dev/null +++ b/packages/pl-api/lib/client/rss-feed-subscriptions.ts @@ -0,0 +1,45 @@ +import * as v from 'valibot'; + +import { rssFeedSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; + +type EmptyObject = Record; + +const rssFeedSubscriptions = (client: PlApiBaseClient) => ({ + /** + * Requires features{@link Features.rssFeedSubscriptions}. + */ + fetchRssFeedSubscriptions: async () => { + const response = await client.request('/api/v1/pleroma/rss_feed_subscriptions'); + + return v.parse(filteredArray(rssFeedSchema), response.json); + }, + + /** + * Requires features{@link Features.rssFeedSubscriptions}. + */ + createRssFeedSubscription: async (url: string) => { + const response = await client.request('/api/v1/pleroma/rss_feed_subscriptions', { + method: 'POST', + body: { url }, + }); + + return v.parse(rssFeedSchema, response.json); + }, + + /** + * Requires features{@link Features.rssFeedSubscriptions}. + */ + deleteRssFeedSubscription: async (url: string) => { + const response = await client.request('/api/v1/pleroma/rss_feed_subscriptions', { + method: 'DELETE', + body: { url }, + }); + + return response.json; + }, +}); + +export { rssFeedSubscriptions }; diff --git a/packages/pl-api/lib/client/scheduled-statuses.ts b/packages/pl-api/lib/client/scheduled-statuses.ts new file mode 100644 index 000000000..d0b43b785 --- /dev/null +++ b/packages/pl-api/lib/client/scheduled-statuses.ts @@ -0,0 +1,57 @@ +import * as v from 'valibot'; + +import { scheduledStatusSchema } from '../entities'; + +import type { PlApiBaseClient } from '../client-base'; +import type { GetScheduledStatusesParams } from '../params/scheduled-statuses'; + +type EmptyObject = Record; + +const scheduledStatuses = (client: PlApiBaseClient) => ({ + /** + * View scheduled statuses + * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#get} + */ + getScheduledStatuses: (params?: GetScheduledStatusesParams) => + client.paginatedGet('/api/v1/scheduled_statuses', { params }, scheduledStatusSchema), + + /** + * View a single scheduled status + * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#get-one} + */ + getScheduledStatus: async (scheduledStatusId: string) => { + const response = await client.request(`/api/v1/scheduled_statuses/${scheduledStatusId}`); + + return v.parse(scheduledStatusSchema, response.json); + }, + + /** + * Update a scheduled status’s publishing date + * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#update} + */ + updateScheduledStatus: async (scheduledStatusId: string, scheduled_at: string) => { + const response = await client.request(`/api/v1/scheduled_statuses/${scheduledStatusId}`, { + method: 'PUT', + body: { scheduled_at }, + }); + + return v.parse(scheduledStatusSchema, response.json); + }, + + /** + * Cancel a scheduled status + * @see {@link https://docs.joinmastodon.org/methods/scheduled_statuses/#cancel} + */ + cancelScheduledStatus: async (scheduledStatusId: string) => { + const response = await client.request( + `/api/v1/scheduled_statuses/${scheduledStatusId}`, + { + method: 'DELETE', + }, + ); + + return response.json; + }, +}); + +export { scheduledStatuses }; diff --git a/packages/pl-api/lib/client/search.ts b/packages/pl-api/lib/client/search.ts new file mode 100644 index 000000000..49699b7a4 --- /dev/null +++ b/packages/pl-api/lib/client/search.ts @@ -0,0 +1,51 @@ +import * as v from 'valibot'; + +import { locationSchema, searchSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; +import { type RequestMeta } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { SearchParams } from '../params/search'; + +const search = (client: PlApiBaseClient) => ({ + /** + * Perform a search + * @see {@link https://docs.joinmastodon.org/methods/search/#v2} + */ + search: async (q: string, params?: SearchParams, meta?: RequestMeta) => { + const response = await client.request('/api/v2/search', { ...meta, params: { ...params, q } }); + + const parsedSearch = v.parse(searchSchema, response.json); + + // A workaround for Pleroma/Akkoma getting into a loop of returning the same account/status when resolve === true. + if (params && params.resolve && params.offset && params.offset > 0) { + const firstAccount = parsedSearch.accounts[0]; + if (firstAccount && [firstAccount.url, firstAccount.acct].includes(q)) { + parsedSearch.accounts = parsedSearch.accounts.slice(1); + } + const firstStatus = parsedSearch.statuses[0]; + if (firstStatus && [firstStatus.uri, firstStatus.url].includes(q)) { + parsedSearch.statuses = parsedSearch.statuses.slice(1); + } + } + + return parsedSearch; + }, + + /** + * Searches for locations + * + * Requires features{@link Features.events}. + * @see {@link https://github.com/mkljczk/pl/blob/fork/docs/development/API/pleroma_api.md#apiv1pleromasearchlocation} + */ + searchLocation: async (q: string, meta?: RequestMeta) => { + const response = await client.request('/api/v1/pleroma/search/location', { + ...meta, + params: { q }, + }); + + return v.parse(filteredArray(locationSchema), response.json); + }, +}); + +export { search }; diff --git a/packages/pl-api/lib/client/settings.ts b/packages/pl-api/lib/client/settings.ts new file mode 100644 index 000000000..ad5009d7b --- /dev/null +++ b/packages/pl-api/lib/client/settings.ts @@ -0,0 +1,823 @@ +import * as v from 'valibot'; + +import { + backupSchema, + credentialAccountSchema, + interactionPoliciesSchema, + oauthTokenSchema, + tokenSchema, +} from '../entities'; +import { coerceObject, filteredArray } from '../entities/utils'; +import { GOTOSOCIAL, ICESHRIMP_NET, MITRA, PIXELFED } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateAccountParams, + UpdateCredentialsParams, + UpdateInteractionPoliciesParams, + UpdateNotificationSettingsParams, +} from '../params/settings'; + +type EmptyObject = Record; + +const settings = (client: PlApiBaseClient) => ({ + /** + * Register an account + * Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox. + * + * Requires features{@link Features.accountCreation} + * @see {@link https://docs.joinmastodon.org/methods/accounts/#create} + */ + createAccount: async (params: CreateAccountParams) => { + const response = await client.request('/api/v1/accounts', { + method: 'POST', + body: { language: params.locale, birthday: params.date_of_birth, ...params }, + }); + + if ('identifier' in response.json) + return v.parse( + v.object({ + message: v.string(), + identifier: v.string(), + }), + response.json, + ); + return v.parse(tokenSchema, response.json); + }, + + /** + * Verify account credentials + * Test to make sure that the user token works. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#verify_credentials} + */ + verifyCredentials: async () => { + const response = await client.request('/api/v1/accounts/verify_credentials'); + + return v.parse(credentialAccountSchema, response.json); + }, + + /** + * Update account credentials + * Update the user’s display and preferences. + * @see {@link https://docs.joinmastodon.org/methods/accounts/#update_credentials} + */ + updateCredentials: async (params: UpdateCredentialsParams) => { + if (params.background_image) { + (params as any).pleroma_background_image = params.background_image; + delete params.background_image; + } + + if (params.settings_store) { + (params as any).pleroma_settings_store = params.settings_store; + + if (client.features.version.software === MITRA) { + await client.request('/api/v1/settings/client_config', { + method: 'POST', + body: params.settings_store, + }); + } + + delete params.settings_store; + } + + const response = await client.request('/api/v1/accounts/update_credentials', { + method: 'PATCH', + contentType: + client.features.version.software === GOTOSOCIAL || + client.features.version.software === ICESHRIMP_NET || + params.avatar || + params.header + ? '' + : undefined, + body: params, + }); + + return v.parse(credentialAccountSchema, response.json); + }, + + /** + * Delete profile avatar + * Deletes the avatar associated with the user’s profile. + * @see {@link https://docs.joinmastodon.org/methods/profile/#delete-profile-avatar} + */ + deleteAvatar: async () => { + const response = await client.request('/api/v1/profile/avatar', { method: 'DELETE' }); + + return v.parse(credentialAccountSchema, response.json); + }, + + /** + * Delete profile header + * Deletes the header image associated with the user’s profile. + * @see {@link https://docs.joinmastodon.org/methods/profile/#delete-profile-header} + */ + deleteHeader: async () => { + const response = await client.request('/api/v1/profile/header', { method: 'DELETE' }); + + return v.parse(credentialAccountSchema, response.json); + }, + + /** + * View user preferences + * Preferences defined by the user in their account settings. + * @see {@link https://docs.joinmastodon.org/methods/preferences/#get} + */ + getPreferences: async () => { + const response = await client.request('/api/v1/preferences'); + + return response.json as Record; + }, + + /** + * Create a user backup archive + * + * Requires features{@link Features.accountBackups}. + */ + createBackup: async () => { + const response = await client.request('/api/v1/pleroma/backups', { method: 'POST' }); + + return v.parse(backupSchema, response.json); + }, + + /** + * List user backups + * + * Requires features{@link Features.accountBackups}. + */ + getBackups: async () => { + const response = await client.request('/api/v1/pleroma/backups'); + + return v.parse(filteredArray(backupSchema), response.json); + }, + + /** + * Get aliases of the current account + * + * Requires features{@link Features.manageAccountAliases}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-aliases-of-the-current-account} + */ + getAccountAliases: async () => { + const response = await client.request('/api/pleroma/aliases'); + + return v.parse(v.object({ aliases: filteredArray(v.string()) }), response.json); + }, + + /** + * Add alias to the current account + * + * Requires features{@link Features.manageAccountAliases}. + * @param alias - the nickname of the alias to add, e.g. foo@example.org. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#add-alias-to-the-current-account} + */ + addAccountAlias: async (alias: string) => { + const response = await client.request('/api/pleroma/aliases', { + method: 'PUT', + body: { alias }, + }); + + return v.parse(v.object({ status: v.literal('success') }), response.json); + }, + + /** + * Delete alias from the current account + * + * Requires features{@link Features.manageAccountAliases}. + * @param alias - the nickname of the alias to add, e.g. foo@example.org. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-alias-from-the-current-account} + */ + deleteAccountAlias: async (alias: string) => { + const response = await client.request('/api/pleroma/aliases', { + method: 'DELETE', + body: { alias }, + }); + + return v.parse(v.object({ status: v.literal('success') }), response.json); + }, + + /** + * Retrieve a list of active sessions for the user + * + * Requires features{@link Features.sessions}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apioauth_tokens} + */ + getOauthTokens: () => { + let url; + + switch (client.features.version.software) { + case GOTOSOCIAL: + url = '/api/v1/tokens'; + break; + case MITRA: + url = '/api/v1/settings/sessions'; + break; + default: + url = '/api/oauth_tokens'; + break; + } + + return client.paginatedGet(url, {}, oauthTokenSchema); + }, + + /** + * Revoke a user session by its ID + * + * Requires features{@link Features.sessions}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apioauth_tokensid} + */ + deleteOauthToken: async (oauthTokenId: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request(`/api/v1/tokens/${oauthTokenId}/invalidate`, { + method: 'POST', + }); + break; + case MITRA: + response = await client.request(`/api/v1/settings/sessions/${oauthTokenId}`, { + method: 'DELETE', + }); + break; + default: + response = await client.request(`/api/oauth_tokens/${oauthTokenId}`, { + method: 'DELETE', + }); + break; + } + + return response.json; + }, + + /** + * Change account password + * + * Requires features{@link Features.changePassword}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger} + * @see {@link https://codeberg.org/silverpill/mitra/src/commit/f15c19527191d82bc3643f984deca43d1527525d/docs/openapi.yaml} + * @see {@link https://git.pleroma.social/pleroma/pleroma/-/blob/develop/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex?ref_type=heads#L68} + */ + changePassword: async (current_password: string, new_password: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user/password_change', { + method: 'POST', + body: { + old_password: current_password, + new_password, + }, + }); + break; + case ICESHRIMP_NET: + await client.getIceshrimpAccessToken(); + response = await client.request('/api/iceshrimp/auth/change-password', { + method: 'POST', + body: { + oldPassword: current_password, + newPassword: new_password, + }, + }); + break; + case MITRA: + response = await client.request('/api/v1/settings/change_password', { + method: 'POST', + body: { new_password }, + }); + break; + case PIXELFED: + response = await client.request('/api/v1.1/accounts/change-password', { + method: 'POST', + body: { + current_password, + new_password, + confirm_password: new_password, + }, + }); + if (response.redirected) throw response; + break; + default: + response = await client.request('/api/pleroma/change_password', { + method: 'POST', + body: { + password: current_password, + new_password, + new_password_confirmation: new_password, + }, + }); + } + + return response.json; + }, + + /** + * Request password reset e-mail + * + * Requires features{@link Features.resetPassword}. + */ + resetPassword: async (email?: string, nickname?: string) => { + const response = await client.request('/auth/password', { + method: 'POST', + body: { email, nickname }, + }); + + return response.json; + }, + + /** + * Requires features{@link Features.changeEmail}. + */ + changeEmail: async (email: string, password: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user/email_change', { + method: 'POST', + body: { + new_email: email, + password, + }, + }); + break; + default: + response = await client.request('/api/pleroma/change_email', { + method: 'POST', + body: { + email, + password, + }, + }); + } + + if (response.json?.error) throw response.json.error; + + return response.json; + }, + + /** + * Requires features{@link Features.deleteAccount}. + */ + deleteAccount: async (password: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/accounts/delete', { + method: 'POST', + body: { password }, + }); + break; + default: + response = await client.request('/api/pleroma/delete_account', { + method: 'POST', + body: { password }, + }); + } + + if (response.json?.error) throw response.json.error; + + return response.json; + }, + + /** + * Requires features{@link Features.deleteAccountWithoutPassword}. + */ + deleteAccountWithoutPassword: async () => { + const response = await client.request('/api/v1/settings/delete_account', { + method: 'POST', + }); + + return response.json; + }, + + /** + * Disable an account + * + * Requires features{@link Features.disableAccount}. + */ + disableAccount: async (password: string) => { + const response = await client.request('/api/pleroma/disable_account', { + method: 'POST', + body: { password }, + }); + + if (response.json?.error) throw response.json.error; + + return response.json; + }, + + /** + * Requires features{@link Features.accountMoving}. + */ + moveAccount: async (target_account: string, password: string) => { + const response = await client.request('/api/pleroma/move_account', { + method: 'POST', + body: { password, target_account }, + }); + + if (response.json?.error) throw response.json.error; + + return response.json; + }, + + mfa: { + /** + * Requires features{@link Features.manageMfa}. + */ + getMfaSettings: async () => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user').then(({ json }) => ({ + settings: { + enabled: !!json?.two_factor_enabled_at, + totp: !!json?.two_factor_enabled_at, + }, + })); + break; + default: + response = (await client.request('/api/pleroma/accounts/mfa')).json; + } + + return v.parse( + v.object({ + settings: coerceObject({ + enabled: v.boolean(), + totp: v.boolean(), + }), + }), + response, + ); + }, + + /** + * Requires features{@link Features.manageMfa}. + */ + getMfaBackupCodes: async () => { + const response = await client.request('/api/pleroma/accounts/mfa/backup_codes'); + + return v.parse( + v.object({ + codes: v.array(v.string()), + }), + response.json, + ); + }, + + /** + * Requires features{@link Features.manageMfa}. + */ + getMfaSetup: async (method: 'totp') => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user/2fa/qruri').then(({ data }) => ({ + provisioning_uri: data, + key: new URL(data).searchParams.get('secret'), + })); + break; + default: + response = (await client.request(`/api/pleroma/accounts/mfa/setup/${method}`)).json; + } + + return v.parse( + v.object({ + key: v.fallback(v.string(), ''), + provisioning_uri: v.string(), + }), + response, + ); + }, + + /** + * Requires features{@link Features.manageMfa}. + */ + confirmMfaSetup: async (method: 'totp', code: string, password: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user/2fa/enable', { + method: 'POST', + body: { code }, + }); + break; + default: + response = ( + await client.request(`/api/pleroma/accounts/mfa/confirm/${method}`, { + method: 'POST', + body: { code, password }, + }) + ).json; + } + + if (response?.error) throw response.error; + + return response as EmptyObject; + }, + + /** + * Requires features{@link Features.manageMfa}. + */ + disableMfa: async (method: 'totp', password: string) => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/user/2fa/disable', { + method: 'POST', + body: { password }, + }); + break; + default: + response = await client.request(`/api/pleroma/accounts/mfa/${method}`, { + method: 'DELETE', + body: { password }, + }); + } + + if (response.json?.error) throw response.json.error; + + return response.json; + }, + }, + + /** + * Imports your follows, for example from a Mastodon CSV file. + * + * Requires features{@link Features.importFollows}. + * `overwrite` mode requires features{@link Features.importOverwrite}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromafollow_import} + */ + importFollows: async (list: File | string, mode?: 'merge' | 'overwrite') => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/import', { + method: 'POST', + body: { data: list, type: 'following', mode }, + contentType: '', + }); + break; + case MITRA: + response = await client.request('/api/v1/settings/import_follows', { + method: 'POST', + body: { follows_csv: typeof list === 'string' ? list : await list.text() }, + }); + break; + default: + response = await client.request('/api/pleroma/follow_import', { + method: 'POST', + body: { list }, + contentType: '', + }); + } + + return response.json; + }, + + /** + * Move followers from remote alias. (experimental?) + * + * Requires features{@link Features.importFollowers}. + */ + importFollowers: async (list: File | string, actorId: string) => { + const response = await client.request('/api/v1/settings/import_followers', { + method: 'POST', + body: { + from_actor_id: actorId, + followers_csv: typeof list === 'string' ? list : await list.text(), + }, + }); + + return response.json; + }, + + /** + * Imports your blocks. + * + * Requires features{@link Features.importBlocks}. + * `overwrite` mode requires features{@link Features.importOverwrite}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromablocks_import} + */ + importBlocks: async (list: File | string, mode?: 'merge' | 'overwrite') => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/import', { + method: 'POST', + body: { data: list, type: 'blocks', mode }, + contentType: '', + }); + break; + default: + response = await client.request('/api/pleroma/blocks_import', { + method: 'POST', + body: { list }, + contentType: '', + }); + } + + return response.json; + }, + + /** + * Imports your mutes. + * + * Requires features{@link Features.importMutes}. + * `overwrite` mode requires features{@link Features.importOverwrite}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromamutes_import} + */ + importMutes: async (list: File | string, mode?: 'merge' | 'overwrite') => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/import', { + method: 'POST', + body: { data: list, type: 'blocks', mode }, + contentType: '', + }); + break; + default: + response = await client.request('/api/pleroma/mutes_import', { + method: 'POST', + body: { list }, + contentType: '', + }); + } + + return response.json; + }, + + /** + * Export followers to CSV file + * + * Requires features{@link Features.exportFollowers}. + */ + exportFollowers: async () => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/exports/followers.csv', { + method: 'GET', + }); + break; + default: + response = await client.request('/api/v1/settings/export_followers', { + method: 'GET', + }); + } + + return response.data; + }, + + /** + * Export follows to CSV file + * + * Requires features{@link Features.exportFollows}. + */ + exportFollows: async () => { + let response; + + switch (client.features.version.software) { + case GOTOSOCIAL: + response = await client.request('/api/v1/exports/following.csv', { + method: 'GET', + }); + break; + default: + response = await client.request('/api/v1/settings/export_follows', { + method: 'GET', + }); + } + + return response.data; + }, + + /** + * Export lists to CSV file + * + * Requires features{@link Features.exportLists}. + */ + exportLists: async () => { + const response = await client.request('/api/v1/exports/lists.csv', { + method: 'GET', + }); + + return response.data; + }, + + /** + * Export blocks to CSV file + * + * Requires features{@link Features.exportBlocks}. + */ + exportBlocks: async () => { + const response = await client.request('/api/v1/exports/blocks.csv', { + method: 'GET', + }); + + return response.data; + }, + + /** + * Export mutes to CSV file + * + * Requires features{@link Features.exportMutes}. + */ + exportMutes: async () => { + const response = await client.request('/api/v1/exports/mutes.csv', { + method: 'GET', + }); + + return response.data; + }, + + /** + * Updates user notification settings + * + * Requires features{@link Features.muteStrangers}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#apipleromanotification_settings} + */ + updateNotificationSettings: async (params: UpdateNotificationSettingsParams) => { + const response = await client.request('/api/pleroma/notification_settings', { + method: 'PUT', + body: params, + }); + + if (response.json?.error) throw response.json.error; + + return v.parse(v.object({ status: v.string() }), response.json); + }, + + /** + * Get default interaction policies for new statuses created by you. + * + * Requires features{@link Features.interactionRequests}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + getInteractionPolicies: async () => { + const response = await client.request('/api/v1/interaction_policies/defaults'); + + return v.parse(interactionPoliciesSchema, response.json); + }, + + /** + * Update default interaction policies per visibility level for new statuses created by you. + * + * Requires features{@link Features.interactionRequests}. + * @see {@link https://docs.gotosocial.org/en/latest/api/swagger/} + */ + updateInteractionPolicies: async (params: UpdateInteractionPoliciesParams) => { + const response = await client.request('/api/v1/interaction_policies/defaults', { + method: 'PATCH', + body: params, + }); + + return v.parse(interactionPoliciesSchema, response.json); + }, + + /** + * List frontend setting profiles + * + * Requires features{@link Features.preferredFrontends}. + */ + getAvailableFrontends: async () => { + const response = await client.request('/api/v1/akkoma/preferred_frontend/available'); + + return v.parse(v.array(v.string()), response.json); + }, + + /** + * Update preferred frontend setting + * + * Store preferred frontend in cookies + * + * Requires features{@link Features.preferredFrontends}. + */ + setPreferredFrontend: async (frontendName: string) => { + const response = await client.request('/api/v1/akkoma/preferred_frontend', { + method: 'PUT', + body: { frontend_name: frontendName }, + }); + + return v.parse(v.object({ frontend_name: v.string() }), response.json); + }, + + authorizeIceshrimp: async () => { + const response = await client.request('/api/v1/accounts/authorize_iceshrimp', { + method: 'POST', + }); + + return response.json; + }, +}); + +export { settings }; diff --git a/packages/pl-api/lib/client/shoutbox.ts b/packages/pl-api/lib/client/shoutbox.ts new file mode 100644 index 000000000..dc5627988 --- /dev/null +++ b/packages/pl-api/lib/client/shoutbox.ts @@ -0,0 +1,69 @@ +import { WebSocket } from 'isows'; +import * as v from 'valibot'; + +import { shoutMessageSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; +import { buildFullPath } from '../utils/url'; + +import type { PlApiBaseClient } from '../client-base'; +import type { ShoutMessage } from '../entities'; + +const shoutbox = (client: PlApiBaseClient) => ({ + connect: ( + token: string, + { + onMessage, + onMessages, + }: { + onMessages: (messages: Array) => void; + onMessage: (message: ShoutMessage) => void; + }, + ) => { + let counter = 2; + let intervalId: NodeJS.Timeout; + if (client.shoutSocket) return client.shoutSocket; + + const path = buildFullPath('/socket/websocket', client.baseURL, { token, vsn: '2.0.0' }); + + const ws = new WebSocket(path); + + ws.onmessage = (event) => { + const [_, __, ___, type, payload] = JSON.parse(event.data as string); + if (type === 'new_msg') { + const message = v.parse(shoutMessageSchema, payload); + onMessage(message); + } else if (type === 'messages') { + const messages = v.parse(filteredArray(shoutMessageSchema), payload.messages); + onMessages(messages); + } + }; + + ws.onopen = () => { + ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'phx_join', {}])); + + intervalId = setInterval(() => { + ws.send(JSON.stringify([null, `${++counter}`, 'phoenix', 'heartbeat', {}])); + }, 5000); + }; + + ws.onclose = () => { + clearInterval(intervalId); + }; + + client.shoutSocket = { + message: (text: string) => { + // guess this is meant to be incremented on each call but idk + ws.send(JSON.stringify(['3', `${++counter}`, 'chat:public', 'new_msg', { text: text }])); + }, + close: () => { + ws.close(); + client.shoutSocket = undefined; + clearInterval(intervalId); + }, + }; + + return client.shoutSocket; + }, +}); + +export { shoutbox }; diff --git a/packages/pl-api/lib/client/statuses.ts b/packages/pl-api/lib/client/statuses.ts new file mode 100644 index 000000000..de7786115 --- /dev/null +++ b/packages/pl-api/lib/client/statuses.ts @@ -0,0 +1,600 @@ +import * as v from 'valibot'; + +import { + accountSchema, + bookmarkFolderSchema, + contextSchema, + emojiReactionSchema, + partialStatusSchema, + scheduledStatusSchema, + statusEditSchema, + statusSchema, + statusSourceSchema, + translationSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; +import { AKKOMA, ICESHRIMP_NET, MITRA, PLEROMA } from '../features'; +import { getAsyncRefreshHeader } from '../request'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateStatusParams, + EditInteractionPolicyParams, + EditStatusParams, + GetFavouritedByParams, + GetRebloggedByParams, + GetStatusContextParams, + GetStatusMentionedUsersParams, + GetStatusParams, + GetStatusQuotesParams, + GetStatusReferencesParams, + GetStatusesParams, +} from '../params/statuses'; + +type EmptyObject = Record; + +const statuses = (client: PlApiBaseClient) => ({ + /** + * Post a new status + * Publish a status with the given parameters. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#create} + */ + createStatus: async (params: CreateStatusParams) => { + type ExtendedCreateStatusParams = CreateStatusParams & { + markdown?: boolean; + circle_id?: string | null; + }; + + const fixedParams: ExtendedCreateStatusParams = params; + + if ( + params.content_type === 'text/markdown' && + client.instanceInformation.api_versions['kmyblue_markdown.fedibird.pl-api'] >= 1 + ) { + fixedParams.markdown = true; + } + if (params.visibility?.startsWith('api/v1/bookmark_categories')) { + fixedParams.circle_id = params.visibility.slice(7); + fixedParams.visibility = 'circle'; + } + if (params.quote_id && client.instanceInformation.api_versions.mastodon >= 7) + params.quoted_status_id = params.quote_id; + else if (params.quoted_status_id && (client.instanceInformation.api_versions.mastodon || 0) < 7) + params.quote_id = params.quoted_status_id; + + const input = + params.preview && client.features.version.software === MITRA + ? '/api/v1/statuses/preview' + : '/api/v1/statuses'; + + const response = await client.request(input, { + method: 'POST', + body: fixedParams, + }); + + if (response.json?.scheduled_at) return v.parse(scheduledStatusSchema, response.json); + return v.parse(statusSchema, response.json); + }, + + /** + * Requires features{@link Features.createStatusPreview}. + */ + previewStatus: async (params: CreateStatusParams) => { + const input = + client.features.version.software === PLEROMA || client.features.version.software === AKKOMA + ? '/api/v1/statuses' + : '/api/v1/statuses/preview'; + + if ( + client.features.version.software === PLEROMA || + client.features.version.software === AKKOMA + ) { + params.preview = true; + } + + const response = await client.request(input, { + method: 'POST', + body: params, + }); + + return v.parse(v.partial(partialStatusSchema), response.json); + }, + + /** + * View a single status + * Obtain information about a status. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#get} + */ + getStatus: async (statusId: string, params?: GetStatusParams) => { + const response = await client.request(`/api/v1/statuses/${statusId}`, { params }); + + return v.parse(statusSchema, response.json); + }, + + /** + * View multiple statuses + * Obtain information about multiple statuses. + * + * Requires features{@link Features.getStatuses}. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#index} + */ + getStatuses: async (statusIds: string[], params?: GetStatusesParams) => { + const response = await client.request('/api/v1/statuses', { + params: { ...params, id: statusIds }, + }); + + return v.parse(filteredArray(statusSchema), response.json); + }, + + /** + * Delete a status + * Delete one of your own statuses. + * + * `delete_media` parameter requires features{@link Features.deleteMedia}. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#delete} + */ + deleteStatus: async (statusId: string, deleteMedia?: boolean) => { + const response = await client.request(`/api/v1/statuses/${statusId}`, { + method: 'DELETE', + params: { delete_media: deleteMedia }, + }); + + return v.parse(statusSourceSchema, response.json); + }, + + /** + * Get parent and child statuses in context + * View statuses above and below this status in the thread. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#context} + */ + getContext: async (statusId: string, params?: GetStatusContextParams) => { + const response = await client.request(`/api/v1/statuses/${statusId}/context`, { params }); + + const asyncRefreshHeader = getAsyncRefreshHeader(response); + + return { asyncRefreshHeader, ...v.parse(contextSchema, response.json) }; + }, + + /** + * Translate a status + * Translate the status content into some language. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#translate} + */ + translateStatus: async (statusId: string, lang?: string) => { + let response; + if (client.features.version.software === AKKOMA) { + response = await client.request(`/api/v1/statuses/${statusId}/translations/${lang}`); + } else { + response = await client.request(`/api/v1/statuses/${statusId}/translate`, { + method: 'POST', + body: { lang }, + }); + } + + return v.parse(translationSchema, response.json); + }, + + /** + * Translate multiple statuses into given language. + * + * Requires features{@link Features.lazyTranslations}. + */ + translateStatuses: async (statusIds: Array, lang: string) => { + const response = await client.request('/api/v1/pl/statuses/translate', { + method: 'POST', + body: { ids: statusIds, lang }, + }); + + return v.parse(filteredArray(translationSchema), response.json); + }, + + /** + * See who boosted a status + * View who boosted a given status. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#reblogged_by} + */ + getRebloggedBy: (statusId: string, params?: GetRebloggedByParams) => + client.paginatedGet(`/api/v1/statuses/${statusId}/reblogged_by`, { params }, accountSchema), + + /** + * See who favourited a status + * View who favourited a given status. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#favourited_by} + */ + getFavouritedBy: (statusId: string, params?: GetFavouritedByParams) => + client.paginatedGet(`/api/v1/statuses/${statusId}/favourited_by`, { params }, accountSchema), + + /** + * Favourite a status + * Add a status to your favourites list. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#favourite} + */ + favouriteStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/favourite`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Undo favourite of a status + * Remove a status from your favourites list. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unfavourite} + */ + unfavouriteStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/unfavourite`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Boost a status + * Reshare a status on your own profile. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#reblog} + * + * Specifying reblog visibility requires features{@link Features.reblogVisibility}. + */ + reblogStatus: async (statusId: string, visibility?: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/reblog`, { + method: 'POST', + body: { visibility }, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Undo boost of a status + * Undo a reshare of a status. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unreblog} + */ + unreblogStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/unreblog`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Bookmark a status + * Privately bookmark a status. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#bookmark} + */ + bookmarkStatus: async (statusId: string, folderId?: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/bookmark`, { + method: 'POST', + body: { folder_id: folderId }, + }); + + if (folderId && client.features.bookmarkFoldersMultiple) { + await client.request(`/api/v1/bookmark_categories/${folderId}/statuses`, { + method: 'POST', + params: { status_ids: [statusId] }, + }); + } + + return v.parse(statusSchema, response.json); + }, + + /** + * Undo bookmark of a status + * Remove a status from your private bookmarks. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unbookmark} + */ + unbookmarkStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/unbookmark`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Revoke a quote post + * Revoke quote authorization of status `quoting_status_id`, detaching status `id`. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#revoke_quote} + */ + revokeQuote: async (statusId: string, quotingStatusId: string) => { + const response = await client.request( + `/api/v1/statuses/${statusId}/quotes/${quotingStatusId}/revoke`, + { method: 'POST' }, + ); + + return v.parse(statusSchema, response.json); + }, + + /** + * Mute a conversation + * Do not receive notifications for the thread that this status is part of. Must be a thread in which you are a participant. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#mute} + */ + muteStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/mute`, { method: 'POST' }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Unmute a conversation + * Start receiving notifications again for the thread that this status is part of. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unmute} + */ + unmuteStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/unmute`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Pin status to profile + * Feature one of your own public statuses at the top of your profile. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#pin} + */ + pinStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/pin`, { method: 'POST' }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Unpin status from profile + * Unfeature a status from the top of your profile. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unpin} + */ + unpinStatus: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/unpin`, { method: 'POST' }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Edit a status + * Edit a given status to change its text, sensitivity, media attachments, or poll. Note that editing a poll’s options will reset the votes. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#unpin} + */ + editStatus: async (statusId: string, params: EditStatusParams) => { + type ExtendedEditStatusParams = EditStatusParams & { + markdown?: boolean; + }; + + const fixedParams: ExtendedEditStatusParams = params; + + if ( + params.content_type === 'text/markdown' && + client.instanceInformation.api_versions['kmyblue_markdown.fedibird.pl-api'] >= 1 + ) { + fixedParams.markdown = true; + } + + const response = await client.request(`/api/v1/statuses/${statusId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Edit a status' interaction policies + * Edit a given status to change its interaction policies. Currently, this means changing its quote approval policy. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#edit_interaction_policy} + */ + editInteractionPolicy: async (statusId: string, params: EditInteractionPolicyParams) => { + const response = await client.request(`/api/v1/statuses/${statusId}`, { + method: 'PUT', + body: params, + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * View edit history of a status + * Get all known versions of a status, including the initial and current states. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#history} + */ + getStatusHistory: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/history`); + + return v.parse(filteredArray(statusEditSchema), response.json); + }, + + /** + * View status source + * Obtain the source properties for a status so that it can be edited. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#source} + */ + getStatusSource: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/source`); + + return v.parse(statusSourceSchema, response.json); + }, + + /** + * Get an object of emoji to account mappings with accounts that reacted to the post + * + * Requires features{@link Features.emojiReactsList}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions} + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactionsemoji} + */ + getStatusReactions: async (statusId: string, emoji?: string) => { + const apiVersions = client.instanceInformation.api_versions; + + let response; + if ( + apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || + client.features.version.software === ICESHRIMP_NET + ) { + response = await client.request( + `/api/v1/pleroma/statuses/${statusId}/reactions${emoji ? `/${emoji}` : ''}`, + ); + } else { + if (apiVersions['emoji_reaction.fedibird.pl-api'] >= 1) { + response = await client.request(`/api/v1/statuses/${statusId}/emoji_reactioned_by`); + } else { + response = await client.request(`/api/v1/statuses/${statusId}/reactions`, { + params: { emoji }, + }); + } + response.json = response.json?.reduce((acc: Array, cur: any) => { + if (emoji && cur.name !== emoji) return acc; + + const existing = acc.find((reaction) => reaction.name === cur.name); + + if (existing) { + existing.accounts.push(cur.account); + existing.account_ids.push(cur.account.id); + existing.count += 1; + } else + acc.push({ count: 1, accounts: [cur.account], account_ids: [cur.account.id], ...cur }); + + return acc; + }, []); + } + + return v.parse(filteredArray(emojiReactionSchema), response?.json || []); + }, + + /** + * React to a post with a unicode emoji + * + * Requires features{@link Features.emojiReacts}. + * Using custom emojis requires features{@link Features.customEmojiReacts}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#put-apiv1pleromastatusesidreactionsemoji} + */ + createStatusReaction: async (statusId: string, emoji: string) => { + const apiVersions = client.instanceInformation.api_versions; + + let response; + if ( + apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || + client.features.version.software === MITRA + ) { + response = await client.request( + `/api/v1/pleroma/statuses/${statusId}/reactions/${encodeURIComponent(emoji)}`, + { method: 'PUT' }, + ); + } else { + response = await client.request( + `/api/v1/statuses/${statusId}/react/${encodeURIComponent(emoji)}`, + { method: 'POST' }, + ); + } + + return v.parse(statusSchema, response.json); + }, + + /** + * Remove a reaction to a post with a unicode emoji + * + * Requires features{@link Features.emojiReacts}. + * @see {@link https://docs.pleroma.social/backend/development/API/pleroma_api/#delete-apiv1pleromastatusesidreactionsemoji} + */ + deleteStatusReaction: async (statusId: string, emoji: string) => { + const apiVersions = client.instanceInformation.api_versions; + + let response; + if ( + apiVersions['emoji_reactions.pleroma.pl-api'] >= 1 || + client.features.version.software === MITRA + ) { + response = await client.request(`/api/v1/pleroma/statuses/${statusId}/reactions/${emoji}`, { + method: 'DELETE', + }); + } else { + response = await client.request( + `/api/v1/statuses/${statusId}/unreact/${encodeURIComponent(emoji)}`, + { method: 'POST' }, + ); + } + + return v.parse(statusSchema, response.json); + }, + + /** + * View quotes for a given status + * + * Requires features{@link Features.quotePosts}. + * @see {@link https://docs.joinmastodon.org/methods/statuses/#quotes} + */ + getStatusQuotes: (statusId: string, params?: GetStatusQuotesParams) => + client.paginatedGet( + client.instanceInformation.api_versions.mastodon >= 7 + ? `/api/v1/statuses/${statusId}/quotes` + : `/api/v1/pleroma/statuses/${statusId}/quotes`, + { params }, + statusSchema, + ), + + /** + * Returns the list of accounts that have disliked the status as known by the current server + * + * Requires features{@link Features.statusDislikes}. + * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#get-apifriendicastatusesiddisliked_by} + */ + getDislikedBy: (statusId: string) => + client.paginatedGet(`/api/v1/statuses/${statusId}/disliked_by`, {}, accountSchema), + + /** + * Marks the given status as disliked by this user + * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#post-apifriendicastatusesiddislike} + */ + dislikeStatus: async (statusId: string) => { + const response = await client.request(`/api/friendica/statuses/${statusId}/dislike`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + /** + * Removes the dislike mark (if it exists) on this status for this user + * @see {@link https://github.com/friendica/friendica/blob/2024.06-rc/doc/API-Friendica.md#post-apifriendicastatusesidundislike} + */ + undislikeStatus: async (statusId: string) => { + const response = await client.request(`/api/friendica/statuses/${statusId}/undislike`, { + method: 'POST', + }); + + return v.parse(statusSchema, response.json); + }, + + getStatusReferences: (statusId: string, params?: GetStatusReferencesParams) => + client.paginatedGet(`/api/v1/statuses/${statusId}/referred_by`, { params }, statusSchema), + + getStatusMentionedUsers: (statusId: string, params?: GetStatusMentionedUsersParams) => + client.paginatedGet(`/api/v1/statuses/${statusId}/mentioned_by`, { params }, accountSchema), + + /** + * Load conversation from a remote server. + * + * Requires features{@link Features.loadConversation}. + */ + loadConversation: async (statusId: string) => { + const response = await client.request( + `/api/v1/statuses/${statusId}/load_conversation`, + { method: 'POST' }, + ); + + return response.json; + }, + + /** + * Requires features{@link Features.bookmarkFoldersMultiple}. + */ + getStatusBookmarkFolders: async (statusId: string) => { + const response = await client.request(`/api/v1/statuses/${statusId}/bookmark_categories`, { + method: 'GET', + }); + + return v.parse(filteredArray(bookmarkFolderSchema), response.json); + }, +}); + +export { statuses }; diff --git a/packages/pl-api/lib/client/stories.ts b/packages/pl-api/lib/client/stories.ts new file mode 100644 index 000000000..eb5ae65ef --- /dev/null +++ b/packages/pl-api/lib/client/stories.ts @@ -0,0 +1,147 @@ +import * as v from 'valibot'; + +import { + accountSchema, + storyCarouselItemSchema, + storyMediaSchema, + storyProfileSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + CreateStoryParams, + CreateStoryPollParams, + CropStoryPhotoParams, + StoryReportType, +} from '../params/stories'; + +type EmptyObject = Record; + +const stories = (client: PlApiBaseClient) => ({ + getRecentStories: async () => { + const response = await client.request('/api/web/stories/v1/recent'); + + return v.parse(filteredArray(storyCarouselItemSchema), response.json); + }, + + getStoryViewers: async (storyId: string) => { + const response = await client.request('/api/web/stories/v1/viewers', { + params: { sid: storyId }, + }); + + return v.parse(filteredArray(accountSchema), response.json); + }, + + getStoriesForProfile: async (accountId: string) => { + const response = await client.request(`/api/web/stories/v1/profile/${accountId}`); + + return v.parse(filteredArray(storyProfileSchema), response.json); + }, + + storyExists: async (accountId: string) => { + const response = await client.request(`/api/web/stories/v1/exists/${accountId}`); + + return v.parse(v.boolean(), response.json); + }, + + getStoryPollResults: async (storyId: string) => { + const response = await client.request('/api/web/stories/v1/poll/results', { + params: { sid: storyId }, + }); + + return v.parse(v.array(v.number()), response.json); + }, + + markStoryAsViewed: async (storyId: string) => { + const response = await client.request('/api/web/stories/v1/viewed', { + method: 'POST', + body: { id: storyId }, + }); + + return response.json; + }, + + createStoryReaction: async (storyId: string, emoji: string) => { + const response = await client.request('/api/web/stories/v1/react', { + method: 'POST', + body: { sid: storyId, reaction: emoji }, + }); + + return response.json; + }, + + createStoryComment: async (storyId: string, comment: string) => { + const response = await client.request('/api/web/stories/v1/comment', { + method: 'POST', + body: { sid: storyId, caption: comment }, + }); + + return response.json; + }, + + createStoryPoll: async (params: CreateStoryPollParams) => { + const response = await client.request('/api/web/stories/v1/publish/poll', { + method: 'POST', + body: params, + }); + + return response.json; + }, + + storyPollVote: async (storyId: string, choiceId: number) => { + const response = await client.request('/api/web/stories/v1/publish/poll', { + method: 'POST', + body: { sid: storyId, ci: choiceId }, + }); + + return response.json; + }, + + reportStory: async (storyId: string, type: StoryReportType) => { + const response = await client.request('/api/web/stories/v1/report', { + method: 'POST', + body: { id: storyId, type }, + }); + + return response.json; + }, + + addMedia: async (file: File) => { + const response = await client.request('/api/web/stories/v1/add', { + method: 'POST', + body: { file }, + contentType: '', + }); + + return v.parse(storyMediaSchema, response.json); + }, + + cropPhoto: async (mediaId: string, params: CropStoryPhotoParams) => { + const response = await client.request('/api/web/stories/v1/crop', { + method: 'POST', + body: { media_id: mediaId, ...params }, + }); + + return response.json; + }, + + createStory: async (mediaId: string, params: CreateStoryParams) => { + const response = await client.request('/api/web/stories/v1/publish', { + method: 'POST', + body: { media_id: mediaId, ...params }, + }); + + return response.json; + }, + + deleteStory: async (storyId: string) => { + const response = await client.request(`/api/web/stories/v1/delete/${storyId}`, { + method: 'DELETE', + }); + + return response.json; + }, +}); + +export { stories }; diff --git a/packages/pl-api/lib/client/streaming.ts b/packages/pl-api/lib/client/streaming.ts new file mode 100644 index 000000000..33950d96e --- /dev/null +++ b/packages/pl-api/lib/client/streaming.ts @@ -0,0 +1,75 @@ +import { WebSocket } from 'isows'; +import * as v from 'valibot'; + +import { streamingEventSchema } from '../entities'; +import { buildFullPath } from '../utils/url'; + +import type { PlApiBaseClient } from '../client-base'; +import type { StreamingEvent } from '../entities'; + +const streaming = (client: PlApiBaseClient) => ({ + /** + * Check if the server is alive + * Verify that the streaming service is alive before connecting to it + * @see {@link https://docs.joinmastodon.org/methods/streaming/#health} + */ + health: async () => { + const response = await client.request('/api/v1/streaming/health'); + + return v.parse(v.literal('OK'), response.json); + }, + + /** + * Establishing a WebSocket connection + * Open a multiplexed WebSocket connection to receive events. + * @see {@link https://docs.joinmastodon.org/methods/streaming/#websocket} + */ + connect: () => { + if (client.socket) return client.socket; + + const path = buildFullPath( + '/api/v1/streaming', + client.instanceInformation?.configuration.urls.streaming, + { access_token: client.accessToken }, + ); + + const ws = new WebSocket(path, client.accessToken as any); + + let listeners: Array<{ listener: (event: StreamingEvent) => any; stream?: string }> = []; + const queue: Array<() => any> = []; + + const enqueue = (fn: () => any) => + ws.readyState === WebSocket.CONNECTING ? queue.push(fn) : fn(); + + ws.onmessage = (event) => { + const message = v.parse(streamingEventSchema, JSON.parse(event.data as string)); + + listeners.filter( + ({ listener, stream }) => (!stream || message.stream.includes(stream)) && listener(message), + ); + }; + + ws.onopen = () => { + queue.forEach((fn) => fn()); + }; + + client.socket = { + listen: (listener: (event: StreamingEvent) => any, stream?: string) => + listeners.push({ listener, stream }), + unlisten: (listener: (event: StreamingEvent) => any) => + (listeners = listeners.filter((value) => value.listener !== listener)), + subscribe: (stream: string, { list, tag }: { list?: string; tag?: string } = {}) => + enqueue(() => ws.send(JSON.stringify({ type: 'subscribe', stream, list, tag }))), + unsubscribe: (stream: string, { list, tag }: { list?: string; tag?: string } = {}) => + enqueue(() => ws.send(JSON.stringify({ type: 'unsubscribe', stream, list, tag }))), + close: () => { + ws.close(); + client.socket = undefined; + }, + }; + + return client.socket; + }, +}); + +export { streaming }; diff --git a/packages/pl-api/lib/client/subscriptions.ts b/packages/pl-api/lib/client/subscriptions.ts new file mode 100644 index 000000000..b2416da9d --- /dev/null +++ b/packages/pl-api/lib/client/subscriptions.ts @@ -0,0 +1,129 @@ +import * as v from 'valibot'; + +import { + accountSchema, + subscriptionDetailsSchema, + subscriptionInvoiceSchema, + subscriptionOptionSchema, +} from '../entities'; +import { filteredArray } from '../entities/utils'; + +import type { PlApiBaseClient } from '../client-base'; + +const subscriptions = (client: PlApiBaseClient) => ({ + /** + * Add subscriber or extend existing subscription. Can be used if blockchain integration is not enabled. + * + * Requires features{@link Features.subscriptions}. + * @param subscriberId - The subscriber ID. + * @param duration - The subscription duration (in seconds). + */ + createSubscription: async (subscriberId: string, duration: number) => { + const response = await client.request('/api/v1/subscriptions', { + method: 'POST', + body: { subscriber_id: subscriberId, duration }, + }); + + return v.parse(subscriptionDetailsSchema, response.json); + }, + + /** + * Get list of subscription options + * + * Requires features{@link Features.subscriptions}. + */ + getSubscriptionOptions: async () => { + const response = await client.request('/api/v1/subscriptions/options'); + + return v.parse(filteredArray(subscriptionOptionSchema), response.json); + }, + + /** + * Enable subscriptions or update subscription settings + * + * Requires features{@link Features.subscriptions}. + * @param type - Subscription type + * @param chainId - CAIP-2 chain ID. + * @param price - Subscription price (only for Monero) + * @param payoutAddress - Payout address (only for Monero) + */ + updateSubscription: async ( + type: 'monero', + chainId?: string, + price?: number, + payoutAddress?: string, + ) => { + const response = await client.request('/api/v1/subscriptions/options', { + method: 'POST', + body: { type, chain_id: chainId, price, payout_address: payoutAddress }, + }); + + return v.parse(accountSchema, response.json); + }, + + /** + * Find subscription by sender and recipient + * + * Requires features{@link Features.subscriptions}. + * @param senderId - Sender ID. + * @param recipientId - Recipient ID. + */ + findSubscription: async (senderId: string, recipientId: string) => { + const response = await client.request('/api/v1/subscriptions/find', { + params: { sender_id: senderId, recipient_id: recipientId }, + }); + + return v.parse(subscriptionDetailsSchema, response.json); + }, + + /** + * Create invoice + * + * Requires features{@link Features.subscriptions}. + * @param senderId - Sender ID. + * @param recipientId - Recipient ID. + * @param chainId - CAIP-2 chain ID. + * @param amount - Requested payment amount (in atomic units). + */ + createInvoice: async (senderId: string, recipientId: string, chainId: string, amount: number) => { + const response = await client.request('/api/v1/subscriptions/invoices', { + method: 'POST', + body: { + sender_id: senderId, + recipient_id: recipientId, + chain_id: chainId, + amount, + }, + }); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, + + /** + * View information about an invoice. + * + * Requires features{@link Features.invoices}. + * @param invoiceId - Invoice ID + */ + getInvoice: async (invoiceId: string) => { + const response = await client.request(`/api/v1/subscriptions/invoices/${invoiceId}`); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, + + /** + * Cancel invoice. + * + * Requires features{@link Features.invoices}. + * @param invoiceId - Invoice ID + */ + cancelInvoice: async (invoiceId: string) => { + const response = await client.request(`/api/v1/subscriptions/invoices/${invoiceId}`, { + method: 'DELETE', + }); + + return v.parse(subscriptionInvoiceSchema, response.json); + }, +}); + +export { subscriptions }; diff --git a/packages/pl-api/lib/client/timelines.ts b/packages/pl-api/lib/client/timelines.ts new file mode 100644 index 000000000..afc90c858 --- /dev/null +++ b/packages/pl-api/lib/client/timelines.ts @@ -0,0 +1,150 @@ +import * as v from 'valibot'; + +import { conversationSchema, markersSchema, statusSchema } from '../entities'; +import { PIXELFED } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { + AntennaTimelineParams, + BubbleTimelineParams, + GetConversationsParams, + GroupTimelineParams, + HashtagTimelineParams, + HomeTimelineParams, + LinkTimelineParams, + ListTimelineParams, + PublicTimelineParams, + SaveMarkersParams, + WrenchedTimelineParams, +} from '../params/timelines'; + +type EmptyObject = Record; + +const timelines = (client: PlApiBaseClient) => ({ + /** + * View public timeline + * View public statuses. + * @see {@link https://docs.joinmastodon.org/methods/timelines/#public} + */ + publicTimeline: (params?: PublicTimelineParams) => + client.paginatedGet('/api/v1/timelines/public', { params }, statusSchema), + + /** + * View hashtag timeline + * View public statuses containing the given hashtag. + * @see {@link https://docs.joinmastodon.org/methods/timelines/#tag} + */ + hashtagTimeline: (hashtag: string, params?: HashtagTimelineParams) => + client.paginatedGet(`/api/v1/timelines/tag/${hashtag}`, { params }, statusSchema), + + /** + * View home timeline + * View statuses from followed users and hashtags. + * @see {@link https://docs.joinmastodon.org/methods/timelines/#home} + */ + homeTimeline: (params?: HomeTimelineParams) => + client.paginatedGet('/api/v1/timelines/home', { params }, statusSchema), + + /** + * View link timeline + * View public statuses containing a link to the specified currently-trending article. This only lists statuses from people who have opted in to discoverability features. + * @see {@link https://docs.joinmastodon.org/methods/timelines/#link} + */ + linkTimeline: (url: string, params?: LinkTimelineParams) => + client.paginatedGet('/api/v1/timelines/link', { params: { ...params, url } }, statusSchema), + + /** + * View list timeline + * View statuses in the given list timeline. + * @see {@link https://docs.joinmastodon.org/methods/timelines/#list} + */ + listTimeline: (listId: string, params?: ListTimelineParams) => + client.paginatedGet(`/api/v1/timelines/list/${listId}`, { params }, statusSchema), + + /** + * View all conversations + * @see {@link https://docs.joinmastodon.org/methods/conversations/#get} + */ + getConversations: (params?: GetConversationsParams) => + client.paginatedGet('/api/v1/conversations', { params }, conversationSchema), + + /** + * Remove a conversation + * Removes a conversation from your list of conversations. + * @see {@link https://docs.joinmastodon.org/methods/conversations/#delete} + */ + deleteConversation: async (conversationId: string) => { + const response = await client.request(`/api/v1/conversations/${conversationId}`, { + method: 'DELETE', + }); + + return response.json; + }, + + /** + * Mark a conversation as read + * @see {@link https://docs.joinmastodon.org/methods/conversations/#read} + */ + markConversationRead: async (conversationId: string) => { + const response = await client.request(`/api/v1/conversations/${conversationId}/read`, { + method: 'POST', + }); + + return v.parse(conversationSchema, response.json); + }, + + /** + * Get saved timeline positions + * Get current positions in timelines. + * @see {@link https://docs.joinmastodon.org/methods/markers/#get} + */ + getMarkers: async (timelines?: string[]) => { + const response = await client.request('/api/v1/markers', { params: { timeline: timelines } }); + + return v.parse(markersSchema, response.json); + }, + + /** + * Save your position in a timeline + * Save current position in timeline. + * @see {@link https://docs.joinmastodon.org/methods/markers/#create} + */ + saveMarkers: async (params: SaveMarkersParams) => { + const response = await client.request('/api/v1/markers', { method: 'POST', body: params }); + + return v.parse(markersSchema, response.json); + }, + + /** + * Requires features{@link Features.groups}. + */ + groupTimeline: (groupId: string, params?: GroupTimelineParams) => + client.paginatedGet( + client.features.version.software === PIXELFED + ? `/api/v0/groups/${groupId}/feed` + : `/api/v1/timelines/group/${groupId}`, + { params }, + statusSchema, + ), + + /** + * Requires features{@link Features.bubbleTimeline}. + */ + bubbleTimeline: (params?: BubbleTimelineParams) => + client.paginatedGet('/api/v1/timelines/bubble', { params }, statusSchema), + + /** + * View antenna timeline + * Requires features{@link Features.antennas}. + */ + antennaTimeline: (antennaId: string, params?: AntennaTimelineParams) => + client.paginatedGet(`/api/v1/timelines/antenna/${antennaId}`, { params }, statusSchema), + + /** + * Requires features{@link Features.wrenchedTimeline}. + */ + wrenchedTimeline: (params?: WrenchedTimelineParams) => + client.paginatedGet('/api/v1/pleroma/timelines/wrenched', { params }, statusSchema), +}); + +export { timelines }; diff --git a/packages/pl-api/lib/client/trends.ts b/packages/pl-api/lib/client/trends.ts new file mode 100644 index 000000000..24aa3cb76 --- /dev/null +++ b/packages/pl-api/lib/client/trends.ts @@ -0,0 +1,55 @@ +import * as v from 'valibot'; + +import { statusSchema, tagSchema, trendsLinkSchema } from '../entities'; +import { filteredArray } from '../entities/utils'; +import { PIXELFED } from '../features'; + +import type { PlApiBaseClient } from '../client-base'; +import type { GetTrendingLinks, GetTrendingStatuses, GetTrendingTags } from '../params/trends'; + +const trends = (client: PlApiBaseClient) => ({ + /** + * View trending tags + * Tags that are being used more frequently within the past week. + * @see {@link https://docs.joinmastodon.org/methods/trends/#tags} + */ + getTrendingTags: async (params?: GetTrendingTags) => { + const response = await client.request( + client.features.version.software === PIXELFED + ? '/api/v1.1/discover/posts/hashtags' + : '/api/v1/trends/tags', + { params }, + ); + + return v.parse(filteredArray(tagSchema), response.json); + }, + + /** + * View trending statuses + * Statuses that have been interacted with more than others. + * @see {@link https://docs.joinmastodon.org/methods/trends/#statuses} + */ + getTrendingStatuses: async (params?: GetTrendingStatuses) => { + const response = await client.request( + client.features.version.software === PIXELFED + ? '/api/pixelfed/v2/discover/posts/trending' + : '/api/v1/trends/statuses', + { params }, + ); + + return v.parse(filteredArray(statusSchema), response.json); + }, + + /** + * View trending links + * Links that have been shared more than others. + * @see {@link https://docs.joinmastodon.org/methods/trends/#links} + */ + getTrendingLinks: async (params?: GetTrendingLinks) => { + const response = await client.request('/api/v1/trends/links', { params }); + + return v.parse(filteredArray(trendsLinkSchema), response.json); + }, +}); + +export { trends }; diff --git a/packages/pl-api/lib/client/utils.ts b/packages/pl-api/lib/client/utils.ts new file mode 100644 index 000000000..c2ea8ddb8 --- /dev/null +++ b/packages/pl-api/lib/client/utils.ts @@ -0,0 +1,7 @@ +import type { PlApiBaseClient } from '../client-base'; + +const utils = (client: PlApiBaseClient) => ({ + paginatedGet: client.paginatedGet.bind(client), +}); + +export { utils }; diff --git a/packages/pl-api/lib/main.ts b/packages/pl-api/lib/main.ts index 639273eae..3b7c16c63 100644 --- a/packages/pl-api/lib/main.ts +++ b/packages/pl-api/lib/main.ts @@ -1,4 +1,5 @@ export { default as PlApiClient } from './client'; +export { PlApiBaseClient } from './client-base'; export { PlApiDirectoryClient } from './directory-client'; export { type Response as PlApiResponse } from './request'; export * from './entities'; diff --git a/packages/pl-api/lib/request.ts b/packages/pl-api/lib/request.ts index 3c7ed8d84..05b92a533 100644 --- a/packages/pl-api/lib/request.ts +++ b/packages/pl-api/lib/request.ts @@ -1,9 +1,10 @@ import LinkHeader from 'http-link-header'; import { serialize } from 'object-to-formdata'; -import PlApiClient from './client'; import { buildFullPath } from './utils/url'; +import type { PlApiBaseClient } from './client-base'; + type Response = { headers: Headers; ok: boolean; @@ -84,7 +85,7 @@ type RequestMeta = Pick( this: Pick< - PlApiClient, + PlApiBaseClient, 'accessToken' | 'customAuthorizationToken' | 'iceshrimpAccessToken' | 'baseURL' >, input: URL | RequestInfo, From 4d10b10aec49aa93fda4df366926d31249dc30ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 10:52:12 +0100 Subject: [PATCH 013/264] pl-api: release 1.0.0-rc.98 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 9488282fe..aa0383ccb 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -1,6 +1,6 @@ { "name": "pl-api", - "version": "1.0.0-rc.97", + "version": "1.0.0-rc.98", "homepage": "https://codeberg.org/mkljczk/nicolium/src/branch/develop/packages/pl-api", "bugs": { "url": "https://codeberg.org/mkljczk/nicolium/issues" From 9a953f512589d9adbe8d479dc6d67d54d4f1651e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 10:52:42 +0100 Subject: [PATCH 014/264] nicolium: allow editing circle title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/locales/en.json | 5 ++ .../pl-fe/src/modals/circle-editor-modal.tsx | 55 ++++++++++++++++++- 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 2476038a4..9a509835d 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -418,6 +418,10 @@ "circles.add_to_circle": "Add to circle", "circles.delete": "Delete circle", "circles.edit": "Edit circle", + "circles.edit.error": "Error updating circle", + "circles.edit.save": "Save circle", + "circles.edit.success": "Circle updated successfully", + "circles.edit.title": "Circle title", "circles.new.create": "Add circle", "circles.new.create_title": "Add circle", "circles.new.title_placeholder": "New circle title", @@ -1793,6 +1797,7 @@ "status.filtered": "Filtered", "status.followed_tag": "You’re following {tags}", "status.group": "Posted in {group}", + "status.group.unknown": "Posted in a group", "status.group_mod_delete": "Delete post from group", "status.hide_translation": "Hide translation", "status.interaction_policy.favourite.approval_required": "The author needs to approve your like.", diff --git a/packages/pl-fe/src/modals/circle-editor-modal.tsx b/packages/pl-fe/src/modals/circle-editor-modal.tsx index 3cbfb2786..c117be244 100644 --- a/packages/pl-fe/src/modals/circle-editor-modal.tsx +++ b/packages/pl-fe/src/modals/circle-editor-modal.tsx @@ -1,7 +1,12 @@ import React, { useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import Button from '@/components/ui/button'; import { CardHeader, CardTitle } from '@/components/ui/card'; +import Form from '@/components/ui/form'; +import FormActions from '@/components/ui/form-actions'; +import FormGroup from '@/components/ui/form-group'; +import Input from '@/components/ui/input'; import Modal from '@/components/ui/modal'; import Spinner from '@/components/ui/spinner'; import Stack from '@/components/ui/stack'; @@ -11,14 +16,21 @@ import { useCircle, useCircleAccounts, useRemoveAccountsFromCircle, + useUpdateCircle, } from '@/queries/accounts/use-circles'; import { useAccountSearch } from '@/queries/search/use-search-accounts'; +import toast from '@/toast'; import Account from './list-editor-modal/components/account'; import Search from './list-editor-modal/components/search'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; +const messages = defineMessages({ + success: { id: 'circles.edit.success', defaultMessage: 'Circle updated successfully' }, + error: { id: 'circles.edit.error', defaultMessage: 'Error updating circle' }, +}); + interface CircleEditorModalProps { circleId: string; } @@ -27,18 +39,39 @@ const CircleEditorModal: React.FC = ({ circleId, onClose, }) => { + const intl = useIntl(); + const [searchValue, setSearchValue] = useState(''); const { data: circle } = useCircle(circleId); + const { mutate: updateCircle, isPending: disabled } = useUpdateCircle(circleId); const { data: accountIds = [] } = useCircleAccounts(circleId); const { data: searchAccountIds = [] } = useAccountSearch(searchValue, { following: true, limit: 5, }); + const [title, setTitle] = useState(circle?.title ?? ''); + const { mutate: addToCircle } = useAddAccountsToCircle(circleId); const { mutate: removeFromCircle } = useRemoveAccountsFromCircle(circleId); + const handleSubmit: React.FormEventHandler = (e) => { + e.preventDefault(); + handleUpdate(); + }; + + const handleUpdate = () => { + updateCircle(title, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.success)); + }, + onError: () => { + toast.error(intl.formatMessage(messages.error)); + }, + }); + }; + const onAdd = (accountId: string) => { addToCircle([accountId]); }; @@ -57,6 +90,26 @@ const CircleEditorModal: React.FC = ({ > {circle ? ( + + } + > + { + setTitle(e.target.value); + }} + /> + + + + + + {accountIds.length > 0 ? (
From 5c78f0b3b8e867a4f062a32c36b394f3d0e633f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 10:53:55 +0100 Subject: [PATCH 015/264] nicolium: circle editor modal improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/modals/circle-editor-modal.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/pl-fe/src/modals/circle-editor-modal.tsx b/packages/pl-fe/src/modals/circle-editor-modal.tsx index c117be244..6fbb077c8 100644 --- a/packages/pl-fe/src/modals/circle-editor-modal.tsx +++ b/packages/pl-fe/src/modals/circle-editor-modal.tsx @@ -45,7 +45,7 @@ const CircleEditorModal: React.FC = ({ const { data: circle } = useCircle(circleId); const { mutate: updateCircle, isPending: disabled } = useUpdateCircle(circleId); - const { data: accountIds = [] } = useCircleAccounts(circleId); + const { data: accountIds = [], isFetching: isFetchingAccounts } = useCircleAccounts(circleId); const { data: searchAccountIds = [] } = useAccountSearch(searchValue, { following: true, limit: 5, @@ -106,12 +106,12 @@ const CircleEditorModal: React.FC = ({ {accountIds.length > 0 ? ( -
+
= ({ ))}
+ ) : isFetchingAccounts ? ( +
+ +
) : ( - - - +
+ + + +
)}
From acc724b448f80cb0826f0974e8a552fbbf2bcb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 10:56:06 +0100 Subject: [PATCH 016/264] nicolium: i18n improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../components/list-members-form.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/pl-fe/src/modals/list-editor-modal/components/list-members-form.tsx b/packages/pl-fe/src/modals/list-editor-modal/components/list-members-form.tsx index f58d53092..6beef36b2 100644 --- a/packages/pl-fe/src/modals/list-editor-modal/components/list-members-form.tsx +++ b/packages/pl-fe/src/modals/list-editor-modal/components/list-members-form.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; import { CardHeader, CardTitle } from '@/components/ui/card'; import Spinner from '@/components/ui/spinner'; @@ -15,18 +15,11 @@ import { useAccountSearch } from '@/queries/search/use-search-accounts'; import Account from './account'; import Search from './search'; -const messages = defineMessages({ - addToList: { id: 'lists.account.add', defaultMessage: 'Add to list' }, - removeFromList: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, -}); - interface IListMembersForm { listId: string; } const ListMembersForm: React.FC = ({ listId }) => { - const intl = useIntl(); - const [searchValue, setSearchValue] = useState(''); const { data: accountIds = [], isFetching } = useListAccounts(listId); @@ -50,7 +43,11 @@ const ListMembersForm: React.FC = ({ listId }) => { {accountIds.length > 0 ? (
- + + } + />
{accountIds.map((accountId) => ( @@ -81,7 +78,9 @@ const ListMembersForm: React.FC = ({ listId }) => {
- + } + />
From cc6f29a1465c89d2af3990d30f7314b2bbffe531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 11:04:36 +0100 Subject: [PATCH 017/264] nicolium: irony MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 9a509835d..2898ad51e 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -419,7 +419,7 @@ "circles.delete": "Delete circle", "circles.edit": "Edit circle", "circles.edit.error": "Error updating circle", - "circles.edit.save": "Save circle", + "circles.edit.save": "Update title", "circles.edit.success": "Circle updated successfully", "circles.edit.title": "Circle title", "circles.new.create": "Add circle", From 6e059905b29a3d6b22f88590ed4321d80fe726eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 11:33:42 +0100 Subject: [PATCH 018/264] nicolium: remove unreachable code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/queries/settings/use-account-aliases.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/pl-fe/src/queries/settings/use-account-aliases.ts b/packages/pl-fe/src/queries/settings/use-account-aliases.ts index 82ccd9033..8ba33d5f0 100644 --- a/packages/pl-fe/src/queries/settings/use-account-aliases.ts +++ b/packages/pl-fe/src/queries/settings/use-account-aliases.ts @@ -2,8 +2,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { defineMessages } from 'react-intl'; import { useClient } from '@/hooks/use-client'; -import { useFeatures } from '@/hooks/use-features'; -import { useOwnAccount } from '@/hooks/use-own-account'; import toast from '@/toast'; const messages = defineMessages({ @@ -19,15 +17,11 @@ const messages = defineMessages({ const useAccountAliases = () => { const client = useClient(); - const features = useFeatures(); - const { account } = useOwnAccount(); return useQuery({ queryKey: ['settings', 'accountAliases'], - queryFn: async (): Promise> => { - if (features.accountMoving) return (await client.settings.getAccountAliases()).aliases; - return account?.__meta.pleroma?.also_known_as ?? []; - }, + queryFn: async (): Promise> => + (await client.settings.getAccountAliases()).aliases, }); }; From 1f275497e8853fc02b651ab64d04f9f679ae6e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 12:07:53 +0100 Subject: [PATCH 019/264] nicolium: cleanup, migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../components/chats-page-settings.tsx | 16 +++++++- .../settings/components/messages-settings.tsx | 20 +++++++++- packages/pl-fe/src/queries/accounts.ts | 37 ------------------- .../accounts/use-account-credentials.ts | 37 +++++++++++++++++++ packages/pl-fe/src/reducers/accounts-meta.ts | 4 -- 5 files changed, 69 insertions(+), 45 deletions(-) delete mode 100644 packages/pl-fe/src/queries/accounts.ts create mode 100644 packages/pl-fe/src/queries/accounts/use-account-credentials.ts diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx index 0a8054ff3..f1a12de1a 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx @@ -14,7 +14,7 @@ import Toggle from '@/components/ui/toggle'; import SettingToggle from '@/features/settings/components/setting-toggle'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useOwnAccount } from '@/hooks/use-own-account'; -import { useUpdateCredentials } from '@/queries/accounts'; +import { useUpdateCredentials } from '@/queries/accounts/use-account-credentials'; import { useSettings } from '@/stores/settings'; type FormData = { @@ -34,6 +34,11 @@ const messages = defineMessages({ defaultMessage: 'Play a sound when you receive a message', }, submit: { id: 'chat.page_settings.submit', defaultMessage: 'Save' }, + success: { + id: 'settings.messages.success', + defaultMessage: 'Chat settings updated successfully', + }, + fail: { id: 'settings.messages.fail', defaultMessage: 'Failed to update chat settings' }, }); const ChatsPageSettings = () => { @@ -55,7 +60,14 @@ const ChatsPageSettings = () => { const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); - updateCredentials.mutate(data); + updateCredentials.mutate(data, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.success)); + }, + onError: () => { + toast.error(intl.formatMessage(messages.fail)); + }, + }); }; return ( diff --git a/packages/pl-fe/src/features/settings/components/messages-settings.tsx b/packages/pl-fe/src/features/settings/components/messages-settings.tsx index d3658582b..4eced926e 100644 --- a/packages/pl-fe/src/features/settings/components/messages-settings.tsx +++ b/packages/pl-fe/src/features/settings/components/messages-settings.tsx @@ -4,13 +4,19 @@ import { defineMessages, useIntl } from 'react-intl'; import List, { ListItem } from '@/components/list'; import Toggle from '@/components/ui/toggle'; import { useOwnAccount } from '@/hooks/use-own-account'; -import { useUpdateCredentials } from '@/queries/accounts'; +import { useUpdateCredentials } from '@/queries/accounts/use-account-credentials'; +import toast from '@/toast'; const messages = defineMessages({ label: { id: 'settings.messages.label', defaultMessage: 'Allow users to start a new chat with you', }, + success: { + id: 'settings.messages.success', + defaultMessage: 'Chat settings updated successfully', + }, + fail: { id: 'settings.messages.fail', defaultMessage: 'Failed to update chat settings' }, }); const MessagesSettings = () => { @@ -19,7 +25,17 @@ const MessagesSettings = () => { const updateCredentials = useUpdateCredentials(); const handleChange = (event: React.ChangeEvent) => { - updateCredentials.mutate({ accepts_chat_messages: event.target.checked }); + updateCredentials.mutate( + { accepts_chat_messages: event.target.checked }, + { + onSuccess: () => { + toast.success(intl.formatMessage(messages.success)); + }, + onError: () => { + toast.error(intl.formatMessage(messages.fail)); + }, + }, + ); }; if (!account) { diff --git a/packages/pl-fe/src/queries/accounts.ts b/packages/pl-fe/src/queries/accounts.ts deleted file mode 100644 index 895a8f74a..000000000 --- a/packages/pl-fe/src/queries/accounts.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; - -import { patchMeSuccess } from '@/actions/me'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useClient } from '@/hooks/use-client'; -import toast from '@/toast'; - -type UpdateCredentialsData = { - accepts_chat_messages?: boolean; -}; - -const useUpdateCredentials = () => { - // const { account } = useOwnAccount(); - const client = useClient(); - const dispatch = useAppDispatch(); - - return useMutation({ - mutationFn: (data: UpdateCredentialsData) => client.settings.updateCredentials(data), - // TODO: What is it intended to do? - // onMutate(variables) { - // const cachedAccount = account; - // dispatch(patchMeSuccess({ ...account, ...variables })); - - // return { cachedAccount }; - // }, - onSuccess(response) { - dispatch(patchMeSuccess(response)); - toast.success('Chat Settings updated successfully'); - }, - onError(_error, _variables, context: any) { - toast.error('Chat Settings failed to update.'); - dispatch(patchMeSuccess(context.cachedAccount)); - }, - }); -}; - -export { useUpdateCredentials }; diff --git a/packages/pl-fe/src/queries/accounts/use-account-credentials.ts b/packages/pl-fe/src/queries/accounts/use-account-credentials.ts new file mode 100644 index 000000000..03f1ec5bf --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/use-account-credentials.ts @@ -0,0 +1,37 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { patchMeSuccess } from '@/actions/me'; +import { useCurrentAccount } from '@/contexts/current-account-context'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; + +import type { UpdateCredentialsParams } from 'pl-api'; + +const useCredentialAccount = () => { + const client = useClient(); + const currentAccount = useCurrentAccount(); + + return useQuery({ + queryKey: [currentAccount, 'credentialAccount'], + queryFn: () => client.settings.verifyCredentials(), + enabled: currentAccount !== 'unauthenticated', + }); +}; + +const useUpdateCredentials = () => { + const client = useClient(); + const currentAccount = useCurrentAccount(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: [currentAccount, 'credentialAccount'], + mutationFn: (params: UpdateCredentialsParams) => client.settings.updateCredentials(params), + onSuccess: (response) => { + queryClient.setQueryData([currentAccount, 'credentialAccount'], response); + dispatch(patchMeSuccess(response)); + }, + }); +}; + +export { useCredentialAccount, useUpdateCredentials }; diff --git a/packages/pl-fe/src/reducers/accounts-meta.ts b/packages/pl-fe/src/reducers/accounts-meta.ts index 94ad40df7..f7a841c60 100644 --- a/packages/pl-fe/src/reducers/accounts-meta.ts +++ b/packages/pl-fe/src/reducers/accounts-meta.ts @@ -15,9 +15,7 @@ import type { Account, CredentialAccount } from 'pl-api'; interface AccountMeta { pleroma: Account['__meta']['pleroma']; - pleromaSource: Account['__meta']['source']; source?: CredentialAccount['source']; - role?: CredentialAccount['role']; } type State = Immutable>; @@ -30,9 +28,7 @@ const importAccount = (state: State, account: CredentialAccount): State => draft[account.id] = { pleroma: account.__meta.pleroma ?? existing?.pleroma, - pleromaSource: account.__meta.source ?? existing?.pleromaSource, source: account.source ?? existing?.source, - role: account.role ?? existing?.role, }; }, { enableAutoFreeze: true }, From 1972ec48102f5bbead4f93721ce14f0a08962a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 12:21:10 +0100 Subject: [PATCH 020/264] nicolium: remove accounts meta reducer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/api/hooks/accounts/use-account.ts | 48 ++++++++++++------ .../accounts/use-account-credentials.ts | 4 +- packages/pl-fe/src/reducers/accounts-meta.ts | 50 ------------------- packages/pl-fe/src/reducers/index.ts | 2 - 4 files changed, 36 insertions(+), 68 deletions(-) delete mode 100644 packages/pl-fe/src/reducers/accounts-meta.ts diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account.ts b/packages/pl-fe/src/api/hooks/accounts/use-account.ts index 7110f7ed3..2a5a75199 100644 --- a/packages/pl-fe/src/api/hooks/accounts/use-account.ts +++ b/packages/pl-fe/src/api/hooks/accounts/use-account.ts @@ -2,10 +2,10 @@ import { useMemo } from 'react'; import { Entities } from '@/entity-store/entities'; import { useEntity } from '@/entity-store/hooks/use-entity'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; import { useLoggedIn } from '@/hooks/use-logged-in'; +import { useCredentialAccount } from '@/queries/accounts/use-account-credentials'; import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; import type { Account } from 'pl-api'; @@ -14,6 +14,18 @@ interface UseAccountOpts { withRelationship?: boolean; } +const ADMIN_PERMISSION = 0x1n; + +const hasAdminPermission = (permissions?: string): boolean | undefined => { + if (!permissions) return undefined; + + try { + return (BigInt(permissions) & ADMIN_PERMISSION) === ADMIN_PERMISSION; + } catch { + return undefined; + } +}; + const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { const client = useClient(); const features = useFeatures(); @@ -26,7 +38,7 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { { enabled: !!accountId }, ); - const meta = useAppSelector((state) => (accountId ? state.accounts_meta[accountId] : undefined)); + const { data: credentialAccount } = useCredentialAccount(me === accountId); const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( withRelationship ? entity?.id : undefined, @@ -35,20 +47,28 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { const isBlocked = entity?.relationship?.blocked_by === true; const isUnavailable = me === entity?.id ? false : isBlocked && !features.blockersVisible; - const account = useMemo( - () => - entity - ? { - ...entity, - relationship, - __meta: { meta, ...entity.__meta }, - // @ts-ignore - is_admin: meta?.role ? (meta.role.permissions & 0x1) === 0x1 : entity.is_admin, - } - : undefined, - [entity, relationship], + const credentialIsAdmin = useMemo( + () => hasAdminPermission(credentialAccount?.role?.permissions), + [credentialAccount?.role?.permissions], ); + const account = useMemo(() => { + if (!entity) return undefined; + + const mergedRelationship = relationship ?? entity.relationship; + const mergedIsAdmin = credentialIsAdmin ?? entity.is_admin; + + if (mergedRelationship === entity.relationship && mergedIsAdmin === entity.is_admin) { + return entity; + } + + return { + ...entity, + relationship: mergedRelationship, + is_admin: mergedIsAdmin, + }; + }, [entity, relationship, credentialIsAdmin]); + return { ...result, isRelationshipLoading, diff --git a/packages/pl-fe/src/queries/accounts/use-account-credentials.ts b/packages/pl-fe/src/queries/accounts/use-account-credentials.ts index 03f1ec5bf..97675c457 100644 --- a/packages/pl-fe/src/queries/accounts/use-account-credentials.ts +++ b/packages/pl-fe/src/queries/accounts/use-account-credentials.ts @@ -7,14 +7,14 @@ import { useClient } from '@/hooks/use-client'; import type { UpdateCredentialsParams } from 'pl-api'; -const useCredentialAccount = () => { +const useCredentialAccount = (enabled = true) => { const client = useClient(); const currentAccount = useCurrentAccount(); return useQuery({ queryKey: [currentAccount, 'credentialAccount'], queryFn: () => client.settings.verifyCredentials(), - enabled: currentAccount !== 'unauthenticated', + enabled: currentAccount !== 'unauthenticated' && enabled, }); }; diff --git a/packages/pl-fe/src/reducers/accounts-meta.ts b/packages/pl-fe/src/reducers/accounts-meta.ts deleted file mode 100644 index f7a841c60..000000000 --- a/packages/pl-fe/src/reducers/accounts-meta.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Accounts Meta: private user data only the owner should see. - * @module pl-fe/reducers/accounts_meta - */ -import { create, type Immutable } from 'mutative'; - -import { - VERIFY_CREDENTIALS_SUCCESS, - AUTH_ACCOUNT_REMEMBER_SUCCESS, - type AuthAction, -} from '@/actions/auth'; -import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from '@/actions/me'; - -import type { Account, CredentialAccount } from 'pl-api'; - -interface AccountMeta { - pleroma: Account['__meta']['pleroma']; - source?: CredentialAccount['source']; -} - -type State = Immutable>; - -const importAccount = (state: State, account: CredentialAccount): State => - create( - state, - (draft) => { - const existing = draft[account.id]; - - draft[account.id] = { - pleroma: account.__meta.pleroma ?? existing?.pleroma, - source: account.source ?? existing?.source, - }; - }, - { enableAutoFreeze: true }, - ); - -const accounts_meta = (state: Readonly = {}, action: AuthAction | MeAction): State => { - switch (action.type) { - case ME_FETCH_SUCCESS: - case ME_PATCH_SUCCESS: - return importAccount(state, action.me); - case VERIFY_CREDENTIALS_SUCCESS: - case AUTH_ACCOUNT_REMEMBER_SUCCESS: - return importAccount(state, action.account); - default: - return state; - } -}; - -export { accounts_meta as default }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 538a1e534..3ba8d9eb8 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -4,7 +4,6 @@ import { AUTH_LOGGED_OUT } from '@/actions/auth'; import * as BuildConfig from '@/build-config'; import entities from '@/entity-store/reducer'; -import accounts_meta from './accounts-meta'; import admin from './admin'; import auth from './auth'; import compose from './compose'; @@ -22,7 +21,6 @@ import statuses from './statuses'; import timelines from './timelines'; const reducers = { - accounts_meta, admin, auth, compose, From 76ec516b2dad9d44bed08988eb6345febe8940b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 14:26:56 +0100 Subject: [PATCH 021/264] nicolium: migrate pending statuses store MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/statuses.ts | 7 +- packages/pl-fe/src/actions/timelines.ts | 4 +- .../features/ui/components/pending-status.tsx | 4 +- packages/pl-fe/src/reducers/index.ts | 2 - .../pl-fe/src/reducers/pending-statuses.ts | 69 ------------------- packages/pl-fe/src/stores/pending-statuses.ts | 61 ++++++++++++++++ 6 files changed, 73 insertions(+), 74 deletions(-) delete mode 100644 packages/pl-fe/src/reducers/pending-statuses.ts create mode 100644 packages/pl-fe/src/stores/pending-statuses.ts diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index bb8e5c767..005794f65 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -1,6 +1,7 @@ import { queryClient } from '@/queries/client'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useModalsStore } from '@/stores/modals'; +import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; import { isLoggedIn } from '@/utils/auth'; import { shouldHaveCard } from '@/utils/status'; @@ -50,7 +51,8 @@ const createStatus = redacting = false, ) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!params.preview) + if (!params.preview) { + usePendingStatusesStore.getState().actions.importStatus(params, idempotencyKey); dispatch({ type: STATUS_CREATE_REQUEST, params, @@ -58,6 +60,7 @@ const createStatus = editing: !!editedId, redacting, }); + } const client = getClient(getState()); @@ -116,6 +119,7 @@ const createStatus = return status; }) .catch((error) => { + usePendingStatusesStore.getState().actions.deleteStatus(idempotencyKey); dispatch({ type: STATUS_CREATE_FAIL, error, @@ -195,6 +199,7 @@ const deleteStatus = : getClient(state).statuses.deleteStatus(statusId) ) .then((response) => { + usePendingStatusesStore.getState().actions.deleteStatus(statusId); dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); dispatch(deleteFromTimelines(statusId)); diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 79a0e88da..ca72eedd3 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -1,4 +1,5 @@ import { getLocale } from '@/actions/settings'; +import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; import { shouldFilter } from '@/utils/timelines'; @@ -39,7 +40,8 @@ const processTimelineUpdate = (timeline: string, status: BaseStatus) => (dispatch: AppDispatch, getState: () => RootState) => { const me = getState().me; const ownStatus = status.account?.id === me; - const hasPendingStatuses = !!getState().pending_statuses.length; + + const hasPendingStatuses = Object.keys(usePendingStatusesStore.getState().statuses).length > 0; const columnSettings = useSettingsStore.getState().settings.timelines[timeline]; const shouldSkipQueue = shouldFilter( diff --git a/packages/pl-fe/src/features/ui/components/pending-status.tsx b/packages/pl-fe/src/features/ui/components/pending-status.tsx index 3ad1ab47e..f35365550 100644 --- a/packages/pl-fe/src/features/ui/components/pending-status.tsx +++ b/packages/pl-fe/src/features/ui/components/pending-status.tsx @@ -11,6 +11,7 @@ import PlaceholderCard from '@/features/placeholder/components/placeholder-card' import PlaceholderMediaGallery from '@/features/placeholder/components/placeholder-media-gallery'; import QuotedStatus from '@/features/status/containers/quoted-status-container'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { usePendingStatus } from '@/stores/pending-statuses'; import { buildStatus } from '../util/pending-status-builder'; @@ -48,8 +49,9 @@ const PendingStatus: React.FC = ({ muted, variant = 'rounded', }) => { + const pendingStatus = usePendingStatus(idempotencyKey); + const status = useAppSelector((state) => { - const pendingStatus = state.pending_statuses[idempotencyKey]; return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null; }); diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 3ba8d9eb8..68bb0fdaf 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -15,7 +15,6 @@ import instance from './instance'; import me from './me'; import meta from './meta'; import notifications from './notifications'; -import pending_statuses from './pending-statuses'; import push_notifications from './push-notifications'; import statuses from './statuses'; import timelines from './timelines'; @@ -33,7 +32,6 @@ const reducers = { me, meta, notifications, - pending_statuses, push_notifications, statuses, timelines, diff --git a/packages/pl-fe/src/reducers/pending-statuses.ts b/packages/pl-fe/src/reducers/pending-statuses.ts deleted file mode 100644 index 167d79422..000000000 --- a/packages/pl-fe/src/reducers/pending-statuses.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { create } from 'mutative'; -import { CreateStatusParams } from 'pl-api'; - -import { - STATUS_CREATE_FAIL, - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, - type StatusesAction, -} from '@/actions/statuses'; - -import type { StatusVisibility } from '@/normalizers/status'; - -interface PendingStatus { - content_type: string; - in_reply_to_id: string | null; - media_ids: Array | null; - quote_id: string | null; - poll: Exclude | null; - sensitive: boolean; - spoiler_text: string; - status: string; - to: Array | null; - visibility: StatusVisibility; -} - -const newPendingStatus = (props: Partial = {}): PendingStatus => ({ - content_type: '', - in_reply_to_id: null, - media_ids: null, - quote_id: null, - poll: null, - sensitive: false, - spoiler_text: '', - status: '', - to: null, - visibility: 'public', - ...props, -}); - -type State = Record; - -const initialState: State = {}; - -const importStatus = (state: State, params: Record, idempotencyKey: string) => { - state[idempotencyKey] = newPendingStatus(params); -}; - -const deleteStatus = (state: State, idempotencyKey: string) => { - delete state[idempotencyKey]; -}; - -const pending_statuses = (state = initialState, action: StatusesAction): State => { - switch (action.type) { - case STATUS_CREATE_REQUEST: - if (action.editing) return state; - return create(state, (draft) => { - importStatus(draft, action.params, action.idempotencyKey); - }); - case STATUS_CREATE_FAIL: - case STATUS_CREATE_SUCCESS: - return create(state, (draft) => { - deleteStatus(draft, action.idempotencyKey); - }); - default: - return state; - } -}; - -export { type PendingStatus, pending_statuses as default }; diff --git a/packages/pl-fe/src/stores/pending-statuses.ts b/packages/pl-fe/src/stores/pending-statuses.ts new file mode 100644 index 000000000..32af2bd5f --- /dev/null +++ b/packages/pl-fe/src/stores/pending-statuses.ts @@ -0,0 +1,61 @@ +import { CreateStatusParams } from 'pl-api'; +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +import type { StatusVisibility } from '@/normalizers/status'; + +interface PendingStatus { + content_type: string; + in_reply_to_id: string | null; + media_ids: Array | null; + quote_id: string | null; + poll: Exclude | null; + sensitive: boolean; + spoiler_text: string; + status: string; + to: Array | null; + visibility: StatusVisibility; +} + +type State = { + statuses: Record; + actions: { + importStatus: (params: Partial, idempotencyKey: string) => void; + deleteStatus: (idempotencyKey: string) => void; + }; +}; + +const usePendingStatusesStore = create()( + mutative((set) => ({ + statuses: {}, + actions: { + importStatus: (params, idempotencyKey) => { + set((state: State) => { + state.statuses[idempotencyKey] = { + content_type: '', + in_reply_to_id: null, + media_ids: null, + quote_id: null, + poll: null, + sensitive: false, + spoiler_text: '', + status: '', + to: null, + visibility: 'public', + ...params, + }; + }); + }, + deleteStatus: (idempotencyKey) => { + set((state: State) => { + delete state.statuses[idempotencyKey]; + }); + }, + }, + })), +); + +const usePendingStatus = (id: string) => usePendingStatusesStore((state) => state.statuses[id]); +const usePendingStatusesActions = () => usePendingStatusesStore((state) => state.actions); + +export { usePendingStatusesStore, usePendingStatus, usePendingStatusesActions }; From 3735f53e91d7740ce9f21a382f7d2bf40b1ec7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 14:31:38 +0100 Subject: [PATCH 022/264] nicolium: i love pushing untested code but hate when it doesn't compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../chats-page/components/chats-page-settings.tsx | 1 + .../pl-fe/src/features/ui/util/pending-status-builder.ts | 2 +- packages/pl-fe/src/stores/pending-statuses.ts | 6 ++---- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx index f1a12de1a..323601774 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx @@ -16,6 +16,7 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useUpdateCredentials } from '@/queries/accounts/use-account-credentials'; import { useSettings } from '@/stores/settings'; +import toast from '@/toast'; type FormData = { accepts_chat_messages?: boolean; diff --git a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts index a0004cc33..2fe033d77 100644 --- a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts +++ b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts @@ -5,8 +5,8 @@ import * as v from 'valibot'; import { normalizeStatus } from '@/normalizers/status'; import { selectOwnAccount } from '@/selectors'; -import type { PendingStatus } from '@/reducers/pending-statuses'; import type { RootState } from '@/store'; +import type { PendingStatus } from '@/stores/pending-statuses'; const buildMentions = (pendingStatus: PendingStatus) => { if (pendingStatus.in_reply_to_id) { diff --git a/packages/pl-fe/src/stores/pending-statuses.ts b/packages/pl-fe/src/stores/pending-statuses.ts index 32af2bd5f..1cbbac4a0 100644 --- a/packages/pl-fe/src/stores/pending-statuses.ts +++ b/packages/pl-fe/src/stores/pending-statuses.ts @@ -2,8 +2,6 @@ import { CreateStatusParams } from 'pl-api'; import { create } from 'zustand'; import { mutative } from 'zustand-mutative'; -import type { StatusVisibility } from '@/normalizers/status'; - interface PendingStatus { content_type: string; in_reply_to_id: string | null; @@ -14,7 +12,7 @@ interface PendingStatus { spoiler_text: string; status: string; to: Array | null; - visibility: StatusVisibility; + visibility: string; } type State = { @@ -58,4 +56,4 @@ const usePendingStatusesStore = create()( const usePendingStatus = (id: string) => usePendingStatusesStore((state) => state.statuses[id]); const usePendingStatusesActions = () => usePendingStatusesStore((state) => state.actions); -export { usePendingStatusesStore, usePendingStatus, usePendingStatusesActions }; +export { usePendingStatusesStore, usePendingStatus, usePendingStatusesActions, type PendingStatus }; From a6208f194ff286bc1f42a6f742e8adab85dccb32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:40:36 +0100 Subject: [PATCH 023/264] nicolium: migrate contexts reducer to zustand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/importer.ts | 5 + packages/pl-fe/src/actions/statuses.ts | 13 + packages/pl-fe/src/actions/timelines.ts | 3 + .../status/components/thread-status.tsx | 5 +- .../src/features/status/components/thread.tsx | 122 ++----- .../src/pages/statuses/event-discussion.tsx | 7 +- .../src/queries/accounts/use-relationship.ts | 23 +- packages/pl-fe/src/reducers/contexts.ts | 242 ------------- packages/pl-fe/src/reducers/index.ts | 2 - packages/pl-fe/src/stores/contexts.ts | 332 ++++++++++++++++++ 10 files changed, 397 insertions(+), 357 deletions(-) delete mode 100644 packages/pl-fe/src/reducers/contexts.ts create mode 100644 packages/pl-fe/src/stores/contexts.ts diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index f1e922f0d..c26342833 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -2,6 +2,7 @@ import { importEntities as importEntityStoreEntities } from '@/entity-store/acti import { Entities } from '@/entity-store/entities'; import { queryClient } from '@/queries/client'; import { selectAccount } from '@/selectors'; +import { useContextStore } from '@/stores/contexts'; import type { AppDispatch, RootState } from '@/store'; import type { @@ -97,6 +98,7 @@ const importEntities = ); if (entities.statuses?.length === 1 && entities.statuses[0] && options.idempotencyKey) { + useContextStore.getState().actions.importStatus(entities.statuses[0], options.idempotencyKey); dispatch({ type: STATUS_IMPORT, status: entities.statuses[0], @@ -132,6 +134,9 @@ const importEntities = ); } } + if (!isEmpty(statuses)) + useContextStore.getState().actions.importStatuses(Object.values(statuses)); + if (!isEmpty(statuses)) dispatch({ type: STATUSES_IMPORT, statuses: Object.values(statuses) }); }; diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 005794f65..6e42df438 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -1,5 +1,6 @@ import { queryClient } from '@/queries/client'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; +import { useContextStore } from '@/stores/contexts'; import { useModalsStore } from '@/stores/modals'; import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; @@ -53,6 +54,7 @@ const createStatus = (dispatch: AppDispatch, getState: () => RootState) => { if (!params.preview) { usePendingStatusesStore.getState().actions.importStatus(params, idempotencyKey); + useContextStore.getState().actions.importPendingStatus(params.in_reply_to_id, idempotencyKey); dispatch({ type: STATUS_CREATE_REQUEST, params, @@ -96,6 +98,13 @@ const createStatus = editing: !!editedId, }); + useContextStore + .getState() + .actions.deletePendingStatus( + 'in_reply_to_id' in status ? status.in_reply_to_id : null, + idempotencyKey, + ); + // Poll the backend for the updated card if (expectsCard) { const delay = 1000; @@ -120,6 +129,9 @@ const createStatus = }) .catch((error) => { usePendingStatusesStore.getState().actions.deleteStatus(idempotencyKey); + useContextStore + .getState() + .actions.deletePendingStatus(params.in_reply_to_id, idempotencyKey); dispatch({ type: STATUS_CREATE_FAIL, error, @@ -241,6 +253,7 @@ const fetchContext = const { ancestors, descendants } = context; const statuses = ancestors.concat(descendants); dispatch(importEntities({ statuses })); + useContextStore.getState().actions.importContext(statusId, context); dispatch({ type: CONTEXT_FETCH_SUCCESS, statusId, ancestors, descendants }); return context; }) diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index ca72eedd3..04de2ced3 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -1,4 +1,5 @@ import { getLocale } from '@/actions/settings'; +import { useContextStore } from '@/stores/contexts'; import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; import { shouldFilter } from '@/utils/timelines'; @@ -131,6 +132,8 @@ const deleteFromTimelines = .map(([key, status]) => [key, status.account_id]); const reblogOf = getState().statuses[statusId]?.reblog_id ?? null; + useContextStore.getState().actions.deleteStatuses([statusId]); + dispatch({ type: TIMELINE_DELETE, statusId, diff --git a/packages/pl-fe/src/features/status/components/thread-status.tsx b/packages/pl-fe/src/features/status/components/thread-status.tsx index 8bcee6e5b..a922540a7 100644 --- a/packages/pl-fe/src/features/status/components/thread-status.tsx +++ b/packages/pl-fe/src/features/status/components/thread-status.tsx @@ -5,6 +5,7 @@ import Tombstone from '@/components/tombstone'; import StatusContainer from '@/containers/status-container'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useReplyCount, useReplyToId } from '@/stores/contexts'; interface IThreadStatus { id: string; @@ -19,8 +20,8 @@ interface IThreadStatus { const ThreadStatus: React.FC = (props): JSX.Element => { const { id, focusedStatusId } = props; - const replyToId = useAppSelector((state) => state.contexts.inReplyTos[id]); - const replyCount = useAppSelector((state) => (state.contexts.replies[id] || []).length); + const replyToId = useReplyToId(id); + const replyCount = useReplyCount(id); const isLoaded = useAppSelector((state) => Boolean(state.statuses[id])); const isDeleted = useAppSelector((state) => Boolean(state.statuses[id]?.deleted)); diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index a2ed16b58..d8e2d81c8 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -1,4 +1,3 @@ -import { createSelector } from '@reduxjs/toolkit'; import { useNavigate } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; @@ -14,14 +13,13 @@ import PlaceholderStatus from '@/features/placeholder/components/placeholder-sta import { Hotkeys } from '@/features/ui/components/hotkeys'; import PendingStatus from '@/features/ui/components/pending-status'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; -import { RootState } from '@/store'; +import { useContextStore, useThread } from '@/stores/contexts'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import { useStatusMetaActions } from '@/stores/status-meta'; @@ -36,102 +34,26 @@ import type { SelectedStatus } from '@/selectors'; import type { Account } from 'pl-api'; import type { VirtuosoHandle } from 'react-virtuoso'; -const makeGetAncestorsIds = () => - createSelector( - [(_: RootState, statusId: string) => statusId, (state: RootState) => state.contexts.inReplyTos], - (statusId, inReplyTos) => { - let ancestorsIds: Array = []; - let id: string = statusId; +const getLinearThreadStatusesIds = ( + statusId: string, + inReplyTos: Record, + replies: Record, +) => { + let parentStatus: string = statusId; - while (id && !ancestorsIds.includes(id)) { - ancestorsIds = [id, ...ancestorsIds]; - id = inReplyTos[id]; - } - - return [...new Set(ancestorsIds)]; - }, - ); - -const makeGetDescendantsIds = () => - createSelector( - [(_: RootState, statusId: string) => statusId, (state: RootState) => state.contexts.replies], - (statusId, contextReplies) => { - let descendantsIds: Array = []; - const ids = [statusId]; - - while (ids.length > 0) { - const id = ids.shift(); - if (!id) break; - - const replies = contextReplies[id]; - - if (descendantsIds.includes(id)) { - break; - } - - if (statusId !== id) { - descendantsIds = [...descendantsIds, id]; - } - - if (replies) { - replies.toReversed().forEach((reply: string) => { - ids.unshift(reply); - }); - } - } - - return [...new Set(descendantsIds)]; - }, - ); - -const makeGetThreadStatusesIds = () => - createSelector( - [ - (_: RootState, statusId: string) => statusId, - (state: RootState) => state.contexts.inReplyTos, - (state: RootState) => state.contexts.replies, - ], - (statusId, inReplyTos, replies) => { - let parentStatus: string = statusId; - - while (inReplyTos[parentStatus]) { - parentStatus = inReplyTos[parentStatus]; - } - - const threadStatuses = [parentStatus]; - - for (let i = 0; i < threadStatuses.length; i++) { - for (const reply of replies[threadStatuses[i]] || []) { - if (!threadStatuses.includes(reply)) threadStatuses.push(reply); - } - } - - return threadStatuses.toSorted(); - }, - ); - -const makeGetThread = (linear = false) => { - if (linear) { - const getThreadStatusesIds = makeGetThreadStatusesIds(); - return (state: RootState, statusId: string) => getThreadStatusesIds(state, statusId); + while (inReplyTos[parentStatus]) { + parentStatus = inReplyTos[parentStatus]; } - const getAncestorsIds = makeGetAncestorsIds(); - const getDescendantsIds = makeGetDescendantsIds(); + const threadStatuses = [parentStatus]; - return createSelector( - [ - (state: RootState, statusId: string) => getAncestorsIds(state, statusId), - (state: RootState, statusId: string) => getDescendantsIds(state, statusId), - (_, statusId: string) => statusId, - ], - (ancestorsIds, descendantsIds, statusId) => { - ancestorsIds = ancestorsIds.filter((id) => id !== statusId && !descendantsIds.includes(id)); - descendantsIds = descendantsIds.filter((id) => id !== statusId && !ancestorsIds.includes(id)); + for (let i = 0; i < threadStatuses.length; i++) { + for (const reply of replies[threadStatuses[i]] || []) { + if (!threadStatuses.includes(reply)) threadStatuses.push(reply); + } + } - return [...ancestorsIds, statusId, ...descendantsIds]; - }, - ); + return threadStatuses.toSorted(); }; interface IThread { @@ -166,10 +88,14 @@ const Thread = ({ const { mutate: unreblogStatus } = useUnreblogStatus(status.id); const linear = displayMode === 'linear'; + const inReplyTos = useContextStore((state) => state.inReplyTos); + const replies = useContextStore((state) => state.replies); + const nestedThread = useThread(status.id); - const getThread = useCallback(makeGetThread(linear), [linear]); - - const thread = useAppSelector((state) => getThread(state, status.id)); + const thread = useMemo( + () => (linear ? getLinearThreadStatusesIds(status.id, inReplyTos, replies) : nestedThread), + [linear, status.id, inReplyTos, replies, nestedThread], + ); const statusIndex = thread.indexOf(status.id); const initialIndex = isModal && statusIndex !== 0 ? statusIndex + 1 : statusIndex; @@ -494,4 +420,4 @@ const Thread = ({ ); }; -export { makeGetDescendantsIds, Thread as default }; +export { Thread as default }; diff --git a/packages/pl-fe/src/pages/statuses/event-discussion.tsx b/packages/pl-fe/src/pages/statuses/event-discussion.tsx index 9fe4ffb54..dca9d28d0 100644 --- a/packages/pl-fe/src/pages/statuses/event-discussion.tsx +++ b/packages/pl-fe/src/pages/statuses/event-discussion.tsx @@ -8,7 +8,6 @@ import ScrollableList from '@/components/scrollable-list'; import Tombstone from '@/components/tombstone'; import Stack from '@/components/ui/stack'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; -import { makeGetDescendantsIds } from '@/features/status/components/thread'; import ThreadStatus from '@/features/status/components/thread-status'; import PendingStatus from '@/features/ui/components/pending-status'; import { eventDiscussionRoute } from '@/features/ui/router'; @@ -16,6 +15,7 @@ import { ComposeForm } from '@/features/ui/util/async-components'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { makeGetStatus } from '@/selectors'; +import { useDescendantsIds } from '@/stores/contexts'; import { selectChild } from '@/utils/scroll-utils'; import type { VirtuosoHandle } from 'react-virtuoso'; @@ -27,14 +27,11 @@ const EventDiscussionPage: React.FC = () => { const dispatch = useAppDispatch(); const getStatus = useCallback(makeGetStatus(), []); - const getDescendantsIds = useCallback(makeGetDescendantsIds(), []); const status = useAppSelector((state) => getStatus(state, { id: statusId })); const me = useAppSelector((state) => state.me); - const descendantsIds = useAppSelector((state) => - getDescendantsIds(state, statusId).filter((id) => id !== statusId), - ); + const descendantsIds = useDescendantsIds(statusId); const [isLoaded, setIsLoaded] = useState(!!status); diff --git a/packages/pl-fe/src/queries/accounts/use-relationship.ts b/packages/pl-fe/src/queries/accounts/use-relationship.ts index 97be4ac37..04552782b 100644 --- a/packages/pl-fe/src/queries/accounts/use-relationship.ts +++ b/packages/pl-fe/src/queries/accounts/use-relationship.ts @@ -9,6 +9,7 @@ import { batcher } from '@/api/batcher'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; import { useLoggedIn } from '@/hooks/use-logged-in'; +import { useContextsActions } from '@/stores/contexts'; import type { MinifiedSuggestion } from '../trends/use-suggested-accounts'; import type { @@ -122,6 +123,7 @@ const useBlockAccountMutation = (accountId: string) => { const client = useClient(); const queryClient = useQueryClient(); const dispatch = useAppDispatch(); + const { filterContexts } = useContextsActions(); return useMutation({ mutationKey: ['accountRelationships', accountId], @@ -155,13 +157,15 @@ const useBlockAccountMutation = (accountId: string) => { }); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - return dispatch((dispatch, getState) => - dispatch({ + return dispatch((dispatch, getState) => { + filterContexts(data, getState().statuses); + + return dispatch({ type: ACCOUNT_BLOCK_SUCCESS, relationship: data, statuses: getState().statuses, - }), - ); + }); + }); }, }); }; @@ -194,6 +198,7 @@ const useMuteAccountMutation = (accountId: string) => { const client = useClient(); const queryClient = useQueryClient(); const dispatch = useAppDispatch(); + const { filterContexts } = useContextsActions(); return useMutation({ mutationKey: ['accountRelationships', accountId], @@ -223,13 +228,15 @@ const useMuteAccountMutation = (accountId: string) => { }); // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers - return dispatch((dispatch, getState) => - dispatch({ + return dispatch((dispatch, getState) => { + filterContexts(data, getState().statuses); + + return dispatch({ type: ACCOUNT_MUTE_SUCCESS, relationship: data, statuses: getState().statuses, - }), - ); + }); + }); }, }); }; diff --git a/packages/pl-fe/src/reducers/contexts.ts b/packages/pl-fe/src/reducers/contexts.ts deleted file mode 100644 index 05ac730f3..000000000 --- a/packages/pl-fe/src/reducers/contexts.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { create } from 'mutative'; - -import { STATUS_IMPORT, STATUSES_IMPORT, type ImporterAction } from '@/actions/importer'; - -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - type AccountsAction, -} from '../actions/accounts'; -import { - CONTEXT_FETCH_SUCCESS, - STATUS_CREATE_REQUEST, - STATUS_CREATE_SUCCESS, - type StatusesAction, -} from '../actions/statuses'; -import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; - -import type { Status } from 'pl-api'; - -interface State { - inReplyTos: Record; - replies: Record>; -} - -const initialState: State = { - inReplyTos: {}, - replies: {}, -}; - -/** Import a single status into the reducer, setting replies and replyTos. */ -const importStatus = ( - state: State, - status: Pick, - idempotencyKey?: string, -) => { - const { id, in_reply_to_id: inReplyToId } = status; - if (!inReplyToId) return; - - const replies = state.replies[inReplyToId] || []; - const newReplies = [...new Set([...replies, id])].toSorted(); - - state.replies[inReplyToId] = newReplies; - state.inReplyTos[id] = inReplyToId; - - if (idempotencyKey) { - deletePendingStatus(state, status.in_reply_to_id, idempotencyKey); - } -}; - -/** Import multiple statuses into the state. */ -const importStatuses = (state: State, statuses: Array>) => { - statuses.forEach((status) => { - importStatus(state, status); - }); -}; - -/** Insert a fake status ID connecting descendant to ancestor. */ -const insertTombstone = (state: State, ancestorId: string, descendantId: string) => { - const tombstoneId = `${descendantId}-tombstone`; - - importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); - importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); -}; - -/** Find the highest level status from this statusId. */ -const getRootNode = (state: State, statusId: string, initialId = statusId): string => { - const parent = state.inReplyTos[statusId]; - - if (!parent) { - return statusId; - } else if (parent === initialId) { - // Prevent cycles - return parent; - } else { - return getRootNode(state, parent, initialId); - } -}; - -/** Route fromId to toId by inserting tombstones. */ -const connectNodes = (state: State, fromId: string, toId: string) => { - const fromRoot = getRootNode(state, fromId); - const toRoot = getRootNode(state, toId); - - if (fromRoot !== toRoot) { - insertTombstone(state, toId, fromId); - return; - } else { - return state; - } -}; - -/** Import a branch of ancestors or descendants, in relation to statusId. */ -const importBranch = ( - state: State, - statuses: Array>, - statusId?: string, -) => { - statuses.forEach((status, i) => { - const prevId = statusId && i === 0 ? statusId : (statuses[i - 1] || {}).id; - - if (status.in_reply_to_id) { - importStatus(state, status); - - // On Mastodon, in_reply_to_id can refer to an unavailable status, - // so traverse the tree up and insert a connecting tombstone if needed. - if (statusId) { - connectNodes(state, status.id, statusId); - } - } else if (prevId) { - // On Pleroma, in_reply_to_id will be null if the parent is unavailable, - // so insert the tombstone now. - insertTombstone(state, prevId, status.id); - } - }); -}; - -/** Import a status's ancestors and descendants. */ -const normalizeContext = ( - state: State, - id: string, - ancestors: Array>, - descendants: Array>, -) => { - importBranch(state, ancestors); - importBranch(state, descendants, id); - - if (ancestors.length > 0 && !state.inReplyTos[id]) { - insertTombstone(state, ancestors[ancestors.length - 1].id, id); - } -}; - -/** Remove a status from the reducer. */ -const deleteStatus = (state: State, statusId: string) => { - // Delete from its parent's tree - const parentId = state.inReplyTos[statusId]; - if (parentId) { - const parentReplies = state.replies[parentId] || []; - const newParentReplies = parentReplies.filter((id) => id !== statusId); - state.replies[parentId] = newParentReplies; - } - - // Dereference children - const replies = (state.replies[statusId] = []); - replies.forEach((reply) => delete state.inReplyTos[reply]); - - delete state.inReplyTos[statusId]; - delete state.replies[statusId]; -}; - -/** Delete multiple statuses from the reducer. */ -const deleteStatuses = (state: State, statusIds: string[]) => { - statusIds.forEach((statusId) => { - deleteStatus(state, statusId); - }); -}; - -/** Delete statuses upon blocking or muting a user. */ -const filterContexts = ( - state: State, - relationship: { id: string }, - /** The entire statuses map from the store. */ - statuses: Record>, -) => { - const ownedStatusIds = Object.values(statuses) - .filter((status) => status.account.id === relationship.id) - .map((status) => status.id); - - deleteStatuses(state, ownedStatusIds); -}; - -/** Add a fake status ID for a pending status. */ -const importPendingStatus = ( - state: State, - inReplyToId: string | null | undefined, - idempotencyKey: string, -) => { - const id = `末pending-${idempotencyKey}`; - importStatus(state, { id, in_reply_to_id: inReplyToId ?? null }); -}; - -/** Delete a pending status from the reducer. */ -const deletePendingStatus = ( - state: State, - inReplyToId: string | null | undefined, - idempotencyKey: string, -) => { - const id = `末pending-${idempotencyKey}`; - - delete state.inReplyTos[id]; - - if (inReplyToId) { - const replies = state.replies[inReplyToId] || []; - const newReplies = replies.filter((replyId) => replyId !== id).toSorted(); - state.replies[inReplyToId] = newReplies; - } -}; - -/** Contexts reducer. Used for building a nested tree structure for threads. */ -const replies = ( - state = initialState, - action: AccountsAction | ImporterAction | StatusesAction | TimelineAction, -): State => { - switch (action.type) { - case ACCOUNT_BLOCK_SUCCESS: - case ACCOUNT_MUTE_SUCCESS: - return create(state, (draft) => { - filterContexts(draft, action.relationship, action.statuses); - }); - case CONTEXT_FETCH_SUCCESS: - return create(state, (draft) => { - normalizeContext(draft, action.statusId, action.ancestors, action.descendants); - }); - case TIMELINE_DELETE: - return create(state, (draft) => { - deleteStatuses(draft, [action.statusId]); - }); - case STATUS_CREATE_REQUEST: - return create(state, (draft) => { - importPendingStatus(draft, action.params.in_reply_to_id, action.idempotencyKey); - }); - case STATUS_CREATE_SUCCESS: - return create(state, (draft) => { - deletePendingStatus( - draft, - 'in_reply_to_id' in action.status ? action.status.in_reply_to_id : null, - action.idempotencyKey, - ); - }); - case STATUS_IMPORT: - return create(state, (draft) => { - importStatus(draft, action.status, action.idempotencyKey); - }); - case STATUSES_IMPORT: - return create(state, (draft) => { - importStatuses(draft, action.statuses); - }); - default: - return state; - } -}; - -export { replies as default }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 68bb0fdaf..789bfdc92 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -7,7 +7,6 @@ import entities from '@/entity-store/reducer'; import admin from './admin'; import auth from './auth'; import compose from './compose'; -import contexts from './contexts'; import conversations from './conversations'; import filters from './filters'; import frontendConfig from './frontend-config'; @@ -23,7 +22,6 @@ const reducers = { admin, auth, compose, - contexts, conversations, entities, filters, diff --git a/packages/pl-fe/src/stores/contexts.ts b/packages/pl-fe/src/stores/contexts.ts new file mode 100644 index 000000000..b35301b34 --- /dev/null +++ b/packages/pl-fe/src/stores/contexts.ts @@ -0,0 +1,332 @@ +import { useMemo } from 'react'; +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +import type { Context, Status } from 'pl-api'; + +/** Minimal status fields needed to process context. */ +type ContextStatus = Pick; + +/** Import a single status into the reducer, setting replies and replyTos. */ +const importStatus = (state: State, status: ContextStatus, idempotencyKey?: string) => { + const { id, in_reply_to_id: inReplyToId } = status; + if (!inReplyToId) return; + + const replies = state.replies[inReplyToId] || []; + const newReplies = [...new Set([...replies, id])].toSorted(); + + state.replies[inReplyToId] = newReplies; + state.inReplyTos[id] = inReplyToId; + + if (idempotencyKey) { + deletePendingStatus(state, status.in_reply_to_id, idempotencyKey); + } +}; + +const importStatuses = (state: State, statuses: ContextStatus[]) => { + statuses.forEach((status) => { + importStatus(state, status); + }); +}; + +/** Insert a fake status ID connecting descendant to ancestor. */ +const insertTombstone = (state: State, ancestorId: string, descendantId: string) => { + const tombstoneId = `${descendantId}-tombstone`; + importStatus(state, { id: tombstoneId, in_reply_to_id: ancestorId }); + importStatus(state, { id: descendantId, in_reply_to_id: tombstoneId }); +}; + +/** Find the highest level status from this statusId. */ +const getRootNode = (state: State, statusId: string, initialId = statusId): string => { + const parent = state.inReplyTos[statusId]; + + if (!parent) { + return statusId; + } else if (parent === initialId) { + // Prevent cycles + return parent; + } else { + return getRootNode(state, parent, initialId); + } +}; + +/** Route fromId to toId by inserting tombstones. */ +const connectNodes = (state: State, fromId: string, toId: string) => { + const fromRoot = getRootNode(state, fromId); + const toRoot = getRootNode(state, toId); + + if (fromRoot !== toRoot) { + insertTombstone(state, toId, fromId); + } +}; + +/** Import a branch of ancestors or descendants, in relation to statusId. */ +const importBranch = (state: State, statuses: ContextStatus[], statusId?: string) => { + statuses.forEach((status, i) => { + const prevId = statusId && i === 0 ? statusId : statuses[i - 1]?.id; + + if (status.in_reply_to_id) { + importStatus(state, status); + + // On Mastodon, in_reply_to_id can refer to an unavailable status, + // so traverse the tree up and insert a connecting tombstone if needed. + if (statusId) { + connectNodes(state, status.id, statusId); + } + } else if (prevId) { + // On Pleroma, in_reply_to_id will be null if the parent is unavailable, + // so insert the tombstone now. + insertTombstone(state, prevId, status.id); + } + }); +}; + +interface State { + inReplyTos: Record; + replies: Record>; + actions: { + /** Delete statuses upon blocking or muting a user. */ + filterContexts: ( + relationship: { id: string }, + statuses: Record, + ) => void; + /** Import a status's ancestors and descendants. */ + importContext: (statusId: string, context: Context) => void; + /** Add a fake status ID for a pending status. */ + importPendingStatus: (inReplyToId: string | null | undefined, idempotencyKey: string) => void; + /** Delete a pending status from the reducer. */ + deletePendingStatus: (inReplyToId: string | null | undefined, idempotencyKey: string) => void; + /** Import a single status into the reducer, setting replies and replyTos. */ + importStatus: (status: ContextStatus, idempotencyKey?: string) => void; + /** Import multiple statuses into the state. */ + importStatuses: (statuses: Array) => void; + /** Delete multiple statuses from the reducer. */ + deleteStatuses: (statusIds: Array) => void; + }; +} + +interface ContextOwnedStatus { + id: string; + account?: { id: string } | null; + account_id?: string | null; +} + +/** Remove a status from the reducer. */ +const deleteStatus = (state: State, statusId: string) => { + const parentId = state.inReplyTos[statusId]; + if (parentId) { + const parentReplies = state.replies[parentId] || []; + const newParentReplies = parentReplies.filter((id) => id !== statusId); + state.replies[parentId] = newParentReplies; + } + + const replies = (state.replies[statusId] = []); + replies.forEach((reply) => delete state.inReplyTos[reply]); + + delete state.inReplyTos[statusId]; + delete state.replies[statusId]; +}; + +/** Delete multiple statuses from the reducer. */ +const deleteStatuses = (state: State, statusIds: string[]) => { + statusIds.forEach((statusId) => { + deleteStatus(state, statusId); + }); +}; + +const getStatusAccountId = (status: ContextOwnedStatus) => status.account_id ?? status.account?.id; + +/** Delete a pending status from the reducer. */ +const deletePendingStatus = ( + state: State, + inReplyToId: string | null | undefined, + idempotencyKey: string, +) => { + const id = `末pending-${idempotencyKey}`; + + delete state.inReplyTos[id]; + + if (inReplyToId) { + const replies = state.replies[inReplyToId] || []; + const newReplies = replies.filter((replyId) => replyId !== id).toSorted(); + state.replies[inReplyToId] = newReplies; + } +}; + +const useContextStore = create()( + mutative((set) => ({ + inReplyTos: {}, + replies: {}, + actions: { + filterContexts: (relationship, statuses) => + set((state) => { + const ownedStatusIds = Object.values(statuses) + .filter((status) => getStatusAccountId(status) === relationship.id) + .map((status) => status.id); + + deleteStatuses(state, ownedStatusIds); + }), + importContext: (statusId: string, { ancestors, descendants }: Context) => + set((state) => { + importBranch(state, ancestors); + importBranch(state, descendants, statusId); + + if (ancestors.length > 0 && !state.inReplyTos[statusId]) { + insertTombstone(state, ancestors[ancestors.length - 1].id, statusId); + } + }), + importPendingStatus: (inReplyToId, idempotencyKey) => + set((state) => { + const id = `末pending-${idempotencyKey}`; + importStatus(state, { id, in_reply_to_id: inReplyToId ?? null }); + }), + deletePendingStatus: (inReplyToId, idempotencyKey) => + set((state) => { + const id = `末pending-${idempotencyKey}`; + + delete state.inReplyTos[id]; + + if (inReplyToId) { + const replies = state.replies[inReplyToId] || []; + const newReplies = replies.filter((replyId) => replyId !== id).toSorted(); + state.replies[inReplyToId] = newReplies; + } + }), + importStatus: (status, idempotencyKey) => + set((state) => { + importStatus(state, status, idempotencyKey); + }), + importStatuses: (statuses) => + set((state) => { + importStatuses(state, statuses); + }), + deleteStatuses: (statusIds) => + set((state) => { + deleteStatuses(state, statusIds); + }), + }, + })), +); + +const getAncestorsIds = (statusId: string, inReplyTos: Record): Array => { + let ancestorsIds: Array = []; + let id: string = statusId; + + while (id && !ancestorsIds.includes(id)) { + ancestorsIds = [id, ...ancestorsIds]; + id = inReplyTos[id]; + } + + return [...new Set(ancestorsIds)]; +}; + +const getDescendantsIds = (statusId: string, contextReplies: Record) => { + let descendantsIds: Array = []; + const ids = [statusId]; + + while (ids.length > 0) { + const id = ids.shift(); + if (!id) break; + + const replies = contextReplies[id]; + + if (descendantsIds.includes(id)) { + break; + } + + if (statusId !== id) { + descendantsIds = [...descendantsIds, id]; + } + + if (replies) { + replies.toReversed().forEach((reply: string) => { + ids.unshift(reply); + }); + } + } + + return [...new Set(descendantsIds)]; +}; + +const useAncestorsIds = (statusId?: string) => { + const inReplyTos = useContextStore((state) => state.inReplyTos); + + return useMemo( + () => (statusId ? getAncestorsIds(statusId, inReplyTos).filter((id) => id !== statusId) : []), + [inReplyTos, statusId], + ); +}; + +const useDescendantsIds = (statusId?: string) => { + const replies = useContextStore((state) => state.replies); + + return useMemo( + () => (statusId ? getDescendantsIds(statusId, replies).filter((id) => id !== statusId) : []), + [replies, statusId], + ); +}; + +const useThread = (statusId?: string, linear?: boolean) => { + const inReplyTos = useContextStore((state) => state.inReplyTos); + const replies = useContextStore((state) => state.replies); + + return useMemo(() => { + if (!statusId) return []; + + if (linear) { + let parentStatus: string = statusId; + + while (inReplyTos[parentStatus]) { + parentStatus = inReplyTos[parentStatus]; + } + + const threadStatuses = [parentStatus]; + + for (let i = 0; i < threadStatuses.length; i++) { + for (const reply of replies[threadStatuses[i]] || []) { + if (!threadStatuses.includes(reply)) threadStatuses.push(reply); + } + } + + return threadStatuses.toSorted(); + } + + let ancestorsIds = getAncestorsIds(statusId, inReplyTos); + let descendantsIds = getDescendantsIds(statusId, replies); + + ancestorsIds = ancestorsIds.filter((id) => id !== statusId && !descendantsIds.includes(id)); + descendantsIds = descendantsIds.filter((id) => id !== statusId && !ancestorsIds.includes(id)); + + return [...ancestorsIds, statusId, ...descendantsIds]; + }, [inReplyTos, replies, statusId, linear]); +}; + +const useReplyToId = (statusId?: string) => { + const inReplyTos = useContextStore((state) => state.inReplyTos); + + return useMemo(() => { + if (!statusId) return undefined; + return inReplyTos[statusId]; + }, [inReplyTos, statusId]); +}; + +const useReplyCount = (statusId?: string) => { + const replies = useContextStore((state) => state.replies); + + return useMemo(() => { + if (!statusId) return 0; + return replies[statusId]?.length || 0; + }, [replies, statusId]); +}; + +const useContextsActions = () => useContextStore((state) => state.actions); + +export { + useContextStore, + useAncestorsIds, + useDescendantsIds, + useThread, + useReplyToId, + useReplyCount, + useContextsActions, +}; From 869556d82dad9841380735941640c7117d688390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:43:13 +0100 Subject: [PATCH 024/264] nicolium: this one's not actually used MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/stores/contexts.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/packages/pl-fe/src/stores/contexts.ts b/packages/pl-fe/src/stores/contexts.ts index b35301b34..7549f2fba 100644 --- a/packages/pl-fe/src/stores/contexts.ts +++ b/packages/pl-fe/src/stores/contexts.ts @@ -248,15 +248,6 @@ const getDescendantsIds = (statusId: string, contextReplies: Record { - const inReplyTos = useContextStore((state) => state.inReplyTos); - - return useMemo( - () => (statusId ? getAncestorsIds(statusId, inReplyTos).filter((id) => id !== statusId) : []), - [inReplyTos, statusId], - ); -}; - const useDescendantsIds = (statusId?: string) => { const replies = useContextStore((state) => state.replies); @@ -323,7 +314,6 @@ const useContextsActions = () => useContextStore((state) => state.actions); export { useContextStore, - useAncestorsIds, useDescendantsIds, useThread, useReplyToId, From 089b979b04203a6e4fd2b01fd5c8091baded4728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:44:09 +0100 Subject: [PATCH 025/264] nicolium: forgot to hit save MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/status/components/thread.tsx | 33 ++----------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index d8e2d81c8..ca87abc40 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -19,7 +19,7 @@ import { useUnfavouriteStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; -import { useContextStore, useThread } from '@/stores/contexts'; +import { useThread } from '@/stores/contexts'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import { useStatusMetaActions } from '@/stores/status-meta'; @@ -34,28 +34,6 @@ import type { SelectedStatus } from '@/selectors'; import type { Account } from 'pl-api'; import type { VirtuosoHandle } from 'react-virtuoso'; -const getLinearThreadStatusesIds = ( - statusId: string, - inReplyTos: Record, - replies: Record, -) => { - let parentStatus: string = statusId; - - while (inReplyTos[parentStatus]) { - parentStatus = inReplyTos[parentStatus]; - } - - const threadStatuses = [parentStatus]; - - for (let i = 0; i < threadStatuses.length; i++) { - for (const reply of replies[threadStatuses[i]] || []) { - if (!threadStatuses.includes(reply)) threadStatuses.push(reply); - } - } - - return threadStatuses.toSorted(); -}; - interface IThread { status: SelectedStatus; withMedia?: boolean; @@ -88,14 +66,7 @@ const Thread = ({ const { mutate: unreblogStatus } = useUnreblogStatus(status.id); const linear = displayMode === 'linear'; - const inReplyTos = useContextStore((state) => state.inReplyTos); - const replies = useContextStore((state) => state.replies); - const nestedThread = useThread(status.id); - - const thread = useMemo( - () => (linear ? getLinearThreadStatusesIds(status.id, inReplyTos, replies) : nestedThread), - [linear, status.id, inReplyTos, replies, nestedThread], - ); + const thread = useThread(status.id, linear); const statusIndex = thread.indexOf(status.id); const initialIndex = isModal && statusIndex !== 0 ? statusIndex + 1 : statusIndex; From f30d8acb78db33a8d3ec85550134bbc1884b8034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:52:00 +0100 Subject: [PATCH 026/264] pl-api: fix feature definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/features.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/pl-api/lib/features.ts b/packages/pl-api/lib/features.ts index f1f4d52f9..5401a12f8 100644 --- a/packages/pl-api/lib/features.ts +++ b/packages/pl-api/lib/features.ts @@ -404,7 +404,9 @@ const getFeatures = (instance: Instance) => { ]), /** Whether people who blocked you are visible through the API. */ - blockersVisible: instance.api_versions['blockers_visible.pleroma.pl-api'] >= 1, + blockersVisible: + !any([v.software === PLEROMA, v.software === AKKOMA]) || + instance.api_versions['blockers_visible.pleroma.pl-api'] >= 1, /** * Ability to specify how long the account block should last. From 1836c4493221b83cc657f17429e69ea3dd0d0c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:53:12 +0100 Subject: [PATCH 027/264] nicolium: remove unreachable code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/pages/accounts/account-timeline.tsx | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/pl-fe/src/pages/accounts/account-timeline.tsx b/packages/pl-fe/src/pages/accounts/account-timeline.tsx index 0d57cf5d4..b6abf27db 100644 --- a/packages/pl-fe/src/pages/accounts/account-timeline.tsx +++ b/packages/pl-fe/src/pages/accounts/account-timeline.tsx @@ -41,8 +41,7 @@ const AccountTimelinePage: React.FC = () => { }), ); - const isBlocked = account?.relationship?.blocked_by; - const unavailable = isBlocked && !features.blockersVisible; + const isBlocked = account?.relationship?.blocked_by && !features.blockersVisible; const isLoading = useAppSelector((state) => state.timelines[`account:${path}`]?.isLoading); const hasMore = useAppSelector((state) => state.timelines[`account:${path}`]?.hasMore); @@ -80,23 +79,16 @@ const AccountTimelinePage: React.FC = () => { return ; } - if (unavailable) { + if (isBlocked) { return ( - {isBlocked ? ( - - ) : ( - - )} + From 1dee0f861ef1c9b931525501c0da9012e0bb9fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 16:55:01 +0100 Subject: [PATCH 028/264] nicolium: fix link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/features/status/components/status-interaction-bar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/features/status/components/status-interaction-bar.tsx b/packages/pl-fe/src/features/status/components/status-interaction-bar.tsx index eb859b4f7..e9588a127 100644 --- a/packages/pl-fe/src/features/status/components/status-interaction-bar.tsx +++ b/packages/pl-fe/src/features/status/components/status-interaction-bar.tsx @@ -72,7 +72,7 @@ const StatusInteractionBar: React.FC = ({ status }): JSX. Date: Mon, 23 Feb 2026 17:02:54 +0100 Subject: [PATCH 029/264] nicolium: fix pleroma username redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/features/ui/router/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index 14767d3df..ec6589bc8 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -1291,7 +1291,7 @@ const redirectPleromaStatusRoute = createRoute({ }); const redirectPleromaUsernameRoute = createRoute({ getParentRoute: () => rootRoute, - path: '/users/@{$username}', + path: '/users/$username', component: () => { const { username } = redirectPleromaUsernameRoute.useParams(); return ; From 059fc9ef982f10876b77d7d78ff06c5e16bc47f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 17:06:54 +0100 Subject: [PATCH 030/264] nicolium: improve disabled dropdown menu item handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../dropdown-menu/dropdown-menu-item.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx index 45221eeda..40912d067 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu-item.tsx @@ -47,7 +47,10 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo event.stopPropagation(); if (!item) return; - if (item.disabled) return; + if (item.disabled) { + event.preventDefault(); + return; + } if (item.items?.length) { event.preventDefault(); @@ -124,7 +127,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo @@ -168,7 +173,12 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo {(item.type === 'toggle' || item.type === 'radio') && (
- +
)} From e9e90875db522ff19b56d3d0d1c6fcc1d8e82bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 17:12:08 +0100 Subject: [PATCH 031/264] nicolium: improve preview card author info MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/components/preview-card.tsx | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index 67606c6df..df8fc9282 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -328,33 +328,44 @@ const PreviewCard: React.FC = ({ ( - - - - {author.account && ( - - )} - - - - - - - )), + name: card.authors.map((author) => { + const linkBody = ( + + {author.account && ( + + )} + + + + + ); + return ( + + {author.account ? ( + + {linkBody} + + ) : ( +
+ {linkBody} + + )} + + ); + }), }} /> From 1b6c941ca79e41612770317270d5d7a32a27eff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 18:06:26 +0100 Subject: [PATCH 032/264] nicolium: migrate conversations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/conversations.ts | 129 -------------- .../api/hooks/streaming/use-user-stream.ts | 4 +- .../conversations/components/conversation.tsx | 21 +-- .../components/conversations-list.tsx | 26 ++- .../src/pages/status-lists/conversations.tsx | 18 +- .../conversations/use-conversations.ts | 166 ++++++++++++++++++ .../use-event-participation-requests.ts | 18 +- .../utils/update-paginated-response.ts | 21 +++ packages/pl-fe/src/reducers/conversations.ts | 138 --------------- packages/pl-fe/src/reducers/index.ts | 2 - 10 files changed, 216 insertions(+), 327 deletions(-) delete mode 100644 packages/pl-fe/src/actions/conversations.ts create mode 100644 packages/pl-fe/src/queries/conversations/use-conversations.ts create mode 100644 packages/pl-fe/src/queries/utils/update-paginated-response.ts delete mode 100644 packages/pl-fe/src/reducers/conversations.ts diff --git a/packages/pl-fe/src/actions/conversations.ts b/packages/pl-fe/src/actions/conversations.ts deleted file mode 100644 index f79b47ff3..000000000 --- a/packages/pl-fe/src/actions/conversations.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { isLoggedIn } from '@/utils/auth'; - -import { getClient } from '../api'; - -import { importEntities } from './importer'; - -import type { AppDispatch, RootState } from '@/store'; -import type { Account, Conversation, PaginatedResponse } from 'pl-api'; - -const CONVERSATIONS_MOUNT = 'CONVERSATIONS_MOUNT' as const; -const CONVERSATIONS_UNMOUNT = 'CONVERSATIONS_UNMOUNT' as const; - -const CONVERSATIONS_FETCH_REQUEST = 'CONVERSATIONS_FETCH_REQUEST' as const; -const CONVERSATIONS_FETCH_SUCCESS = 'CONVERSATIONS_FETCH_SUCCESS' as const; -const CONVERSATIONS_FETCH_FAIL = 'CONVERSATIONS_FETCH_FAIL' as const; -const CONVERSATIONS_UPDATE = 'CONVERSATIONS_UPDATE' as const; - -const CONVERSATIONS_READ = 'CONVERSATIONS_READ' as const; - -const mountConversations = () => ({ type: CONVERSATIONS_MOUNT }); - -const unmountConversations = () => ({ type: CONVERSATIONS_UNMOUNT }); - -interface ConversationsReadAction { - type: typeof CONVERSATIONS_READ; - conversationId: string; -} - -const markConversationRead = - (conversationId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - dispatch({ - type: CONVERSATIONS_READ, - conversationId, - }); - - return getClient(getState).timelines.markConversationRead(conversationId); - }; - -const expandConversations = - (expand = true) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - const state = getState(); - if (state.conversations.isLoading) return; - - const hasMore = state.conversations.hasMore; - if (expand && !hasMore) return; - - dispatch(expandConversationsRequest()); - - return (state.conversations.next?.() ?? getClient(state).timelines.getConversations()) - .then((response) => { - dispatch( - importEntities({ - accounts: response.items.reduce( - (aggr: Array, item) => aggr.concat(item.accounts), - [], - ), - statuses: response.items.map((item) => item.last_status), - }), - ); - dispatch(expandConversationsSuccess(response.items, response.next, expand)); - }) - .catch((err) => dispatch(expandConversationsFail(err))); - }; - -const expandConversationsRequest = () => ({ type: CONVERSATIONS_FETCH_REQUEST }); - -const expandConversationsSuccess = ( - conversations: Conversation[], - next: (() => Promise>) | null, - isLoadingRecent: boolean, -) => ({ - type: CONVERSATIONS_FETCH_SUCCESS, - conversations, - next, - isLoadingRecent, -}); - -const expandConversationsFail = (error: unknown) => ({ - type: CONVERSATIONS_FETCH_FAIL, - error, -}); - -interface ConversataionsUpdateAction { - type: typeof CONVERSATIONS_UPDATE; - conversation: Conversation; -} - -const updateConversations = (conversation: Conversation) => (dispatch: AppDispatch) => { - dispatch( - importEntities({ - accounts: conversation.accounts, - statuses: [conversation.last_status], - }), - ); - - return dispatch({ - type: CONVERSATIONS_UPDATE, - conversation, - }); -}; - -type ConversationsAction = - | ReturnType - | ReturnType - | ConversationsReadAction - | ReturnType - | ReturnType - | ReturnType - | ConversataionsUpdateAction; - -export { - CONVERSATIONS_MOUNT, - CONVERSATIONS_UNMOUNT, - CONVERSATIONS_FETCH_REQUEST, - CONVERSATIONS_FETCH_SUCCESS, - CONVERSATIONS_FETCH_FAIL, - CONVERSATIONS_UPDATE, - CONVERSATIONS_READ, - mountConversations, - unmountConversations, - markConversationRead, - expandConversations, - updateConversations, - type ConversationsAction, -}; diff --git a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts index fb0d0d124..a8d120098 100644 --- a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react'; -import { updateConversations } from '@/actions/conversations'; import { fetchFilters } from '@/actions/filters'; import { MARKER_FETCH_SUCCESS } from '@/actions/markers'; import { updateNotificationsQueue } from '@/actions/notifications'; @@ -12,6 +11,7 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useLoggedIn } from '@/hooks/use-logged-in'; import messages from '@/messages'; import { queryClient } from '@/queries/client'; +import { updateConversations } from '@/queries/conversations/use-conversations'; import { useSettings } from '@/stores/settings'; import { getUnreadChatsCount, updateChatListItem } from '@/utils/chats'; import { play, soundCache } from '@/utils/sounds'; @@ -131,7 +131,7 @@ const useUserStream = () => { }); break; case 'conversation': - dispatch(updateConversations(event.payload)); + updateConversations(event.payload); break; case 'filters_changed': dispatch(fetchFilters()); diff --git a/packages/pl-fe/src/features/conversations/components/conversation.tsx b/packages/pl-fe/src/features/conversations/components/conversation.tsx index b06847e25..52c1aa240 100644 --- a/packages/pl-fe/src/features/conversations/components/conversation.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversation.tsx @@ -1,32 +1,29 @@ import { useNavigate } from '@tanstack/react-router'; import React from 'react'; -import { markConversationRead } from '@/actions/conversations'; import { useAccount } from '@/api/hooks/accounts/use-account'; import StatusContainer from '@/containers/status-container'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; +import { + useMarkConversationRead, + type MinifiedConversation, +} from '@/queries/conversations/use-conversations'; interface IConversation { - conversationId: string; + conversation: MinifiedConversation; onMoveUp: (id: string) => void; onMoveDown: (id: string) => void; } -const Conversation: React.FC = ({ conversationId, onMoveUp, onMoveDown }) => { - const dispatch = useAppDispatch(); +const Conversation: React.FC = ({ conversation, onMoveUp, onMoveDown }) => { const navigate = useNavigate(); - const { - account_ids, - unread, - last_status: lastStatusId, - } = useAppSelector((state) => state.conversations.items.find((x) => x.id === conversationId)!); + const { id: conversationId, account_ids, unread, last_status: lastStatusId } = conversation; + const { mutate: markConversationRead } = useMarkConversationRead(conversationId); const { account: lastStatusAccount } = useAccount(account_ids[0]); const handleClick = () => { if (unread) { - dispatch(markConversationRead(conversationId)); + markConversationRead(); } if (lastStatusId) diff --git a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx index 6f5ec4584..d7a8cdb12 100644 --- a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx @@ -2,10 +2,9 @@ import { debounce } from '@tanstack/react-pacer/debouncer'; import React, { useCallback, useRef } from 'react'; import { FormattedMessage } from 'react-intl'; -import { expandConversations } from '@/actions/conversations'; import ScrollableList from '@/components/scrollable-list'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; +import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; +import { useConversations } from '@/queries/conversations/use-conversations'; import { selectChild } from '@/utils/scroll-utils'; import Conversation from './conversation'; @@ -13,12 +12,9 @@ import Conversation from './conversation'; import type { VirtuosoHandle } from 'react-virtuoso'; const ConversationsList: React.FC = () => { - const dispatch = useAppDispatch(); const ref = useRef(null); - const conversations = useAppSelector((state) => state.conversations.items); - const isLoading = useAppSelector((state) => state.conversations.isLoading); - const hasMore = useAppSelector((state) => state.conversations.hasMore); + const { conversations, isLoading, hasNextPage, isFetching, fetchNextPage } = useConversations(); const getCurrentIndex = (id: string) => conversations.findIndex((x) => x.id === id); @@ -40,22 +36,22 @@ const ConversationsList: React.FC = () => { const handleLoadOlder = useCallback( debounce( () => { - if (hasMore) dispatch(expandConversations()); + if (hasNextPage) fetchNextPage(); }, { wait: 300, leading: true }, ), - [hasMore], + [hasNextPage, fetchNextPage], ); return ( { /> } listClassName='⁂-status-list' + placeholderComponent={PlaceholderStatus} + placeholderCount={20} > - {conversations.map((item: any) => ( + {conversations.map((item) => ( diff --git a/packages/pl-fe/src/pages/status-lists/conversations.tsx b/packages/pl-fe/src/pages/status-lists/conversations.tsx index ebc173310..3a181fc4e 100644 --- a/packages/pl-fe/src/pages/status-lists/conversations.tsx +++ b/packages/pl-fe/src/pages/status-lists/conversations.tsx @@ -1,15 +1,9 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { - mountConversations, - unmountConversations, - expandConversations, -} from '@/actions/conversations'; import { useDirectStream } from '@/api/hooks/streaming/use-direct-stream'; import Column from '@/components/ui/column'; import ConversationsList from '@/features/conversations/components/conversations-list'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Direct messages' }, @@ -18,19 +12,9 @@ const messages = defineMessages({ const ConversationsTimeline = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); useDirectStream(); - useEffect(() => { - dispatch(mountConversations()); - dispatch(expandConversations(false)); - - return () => { - dispatch(unmountConversations()); - }; - }, []); - return ( diff --git a/packages/pl-fe/src/queries/conversations/use-conversations.ts b/packages/pl-fe/src/queries/conversations/use-conversations.ts new file mode 100644 index 000000000..11a4c85e1 --- /dev/null +++ b/packages/pl-fe/src/queries/conversations/use-conversations.ts @@ -0,0 +1,166 @@ +import { + type InfiniteData, + useInfiniteQuery, + useMutation, + useQueryClient, +} from '@tanstack/react-query'; +import { create } from 'mutative'; +import { useMemo } from 'react'; + +import { importEntities } from '@/actions/importer'; +import { useClient } from '@/hooks/use-client'; +import { useLoggedIn } from '@/hooks/use-logged-in'; +import { store } from '@/store'; +import { compareDate } from '@/utils/comparators'; + +import { queryClient } from '../client'; +import { updatePaginatedResponse } from '../utils/update-paginated-response'; + +import type { Conversation, PaginatedResponse } from 'pl-api'; + +type MinifiedConversation = { + id: string; + unread: boolean; + account_ids: string[]; + last_status: string | null; + last_status_created_at: string | null; +}; + +type MinifiedConversationPage = PaginatedResponse; + +const minifyConversation = (conversation: Conversation): MinifiedConversation => ({ + id: conversation.id, + unread: conversation.unread, + account_ids: conversation.accounts.map((account) => account.id), + last_status: conversation.last_status?.id ?? null, + last_status_created_at: conversation.last_status?.created_at ?? null, +}); + +const sortConversations = (items: MinifiedConversation[]) => + items.toSorted((a, b) => { + if (a.last_status_created_at === null || b.last_status_created_at === null) { + return -1; + } + + return compareDate(a.last_status_created_at, b.last_status_created_at); + }); + +const importConversationEntities = (conversations: Conversation[]) => { + store.dispatch( + importEntities({ + accounts: conversations.flatMap((conversation) => conversation.accounts), + statuses: conversations.map((conversation) => conversation.last_status), + }) as any, + ); +}; + +const minifyConversationPage = ( + response: PaginatedResponse, +): MinifiedConversationPage => { + importConversationEntities(response.items); + + return { + ...response, + previous: response.previous + ? () => response.previous!().then((page) => minifyConversationPage(page)) + : null, + next: response.next + ? () => response.next!().then((page) => minifyConversationPage(page)) + : null, + items: response.items.map(minifyConversation), + }; +}; + +const updateConversations = (conversation: Conversation) => { + importConversationEntities([conversation]); + + queryClient.setQueryData>(['conversations'], (data) => { + if (!data || !data.pages.length) return data; + + return create(data, (draft) => { + const updatedConversation = minifyConversation(conversation); + + let found = false; + + for (const page of draft.pages) { + const index = page.items.findIndex((item) => item.id === updatedConversation.id); + if (index !== -1) { + page.items[index] = updatedConversation; + found = true; + break; + } + } + + if (!found) { + draft.pages[0].items.unshift(updatedConversation); + } + }); + }); +}; + +const useConversations = () => { + const client = useClient(); + const { isLoggedIn } = useLoggedIn(); + + const query = useInfiniteQuery({ + queryKey: ['conversations'], + queryFn: async ({ pageParam }) => { + if (pageParam.next) { + return pageParam.next(); + } + + const response = await client.timelines.getConversations(); + return minifyConversationPage(response); + }, + initialPageParam: { + previous: null, + next: null, + items: [], + partial: false, + } as MinifiedConversationPage, + getNextPageParam: (page) => (page.next ? page : undefined), + enabled: isLoggedIn, + }); + + const conversations = useMemo( + () => sortConversations(query.data?.pages.flatMap((page) => page.items) ?? []), + [query.data], + ); + + return { ...query, conversations }; +}; + +const useMarkConversationRead = (conversationId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['conversations', conversationId, 'read'], + mutationFn: () => client.timelines.markConversationRead(conversationId), + onMutate: async () => { + await queryClient.cancelQueries({ queryKey: ['conversations'] }); + + const previous = queryClient.getQueryData>([ + 'conversations', + ]); + + updatePaginatedResponse(['conversations'], (items) => + items.map((item) => (item.id === conversationId ? { ...item, unread: false } : item)), + ); + + return { previous }; + }, + onError: (_, __, context) => { + if (context?.previous) { + queryClient.setQueryData(['conversations'], context.previous); + } + }, + }); +}; + +export { + useConversations, + useMarkConversationRead, + updateConversations, + type MinifiedConversation, +}; diff --git a/packages/pl-fe/src/queries/events/use-event-participation-requests.ts b/packages/pl-fe/src/queries/events/use-event-participation-requests.ts index 4a4a76639..c089c7147 100644 --- a/packages/pl-fe/src/queries/events/use-event-participation-requests.ts +++ b/packages/pl-fe/src/queries/events/use-event-participation-requests.ts @@ -1,10 +1,10 @@ -import { type InfiniteData, useMutation } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { importEntities } from '@/actions/importer'; import { useClient } from '@/hooks/use-client'; -import { queryClient } from '@/queries/client'; import { makePaginatedResponseQuery } from '@/queries/utils/make-paginated-response-query'; import { minifyList } from '@/queries/utils/minify-list'; +import { updatePaginatedResponse } from '@/queries/utils/update-paginated-response'; import { store } from '@/store'; import type { PlApiClient } from 'pl-api'; @@ -24,20 +24,12 @@ const minifyRequestList = ( ); type MinifiedRequestList = ReturnType; +type MinifiedRequest = MinifiedRequestList['items'][0]; const removeRequest = (statusId: string, accountId: string) => - queryClient.setQueryData>( + updatePaginatedResponse( ['accountsLists', 'eventParticipationRequests', statusId], - (data) => - data - ? { - ...data, - pages: data.pages.map(({ items, ...page }) => ({ - ...page, - items: items.filter(({ account_id }) => account_id !== accountId), - })), - } - : undefined, + (items) => items.filter(({ account_id }) => account_id !== accountId), ); const useEventParticipationRequests = makePaginatedResponseQuery( diff --git a/packages/pl-fe/src/queries/utils/update-paginated-response.ts b/packages/pl-fe/src/queries/utils/update-paginated-response.ts new file mode 100644 index 000000000..61ef3aabf --- /dev/null +++ b/packages/pl-fe/src/queries/utils/update-paginated-response.ts @@ -0,0 +1,21 @@ +import { type InfiniteData, type QueryKey } from '@tanstack/react-query'; +import { PaginatedResponse } from 'pl-api'; + +import { queryClient } from '@/queries/client'; + +const updatePaginatedResponse = ( + queryKey: QueryKey, + updater: (items: PaginatedResponse['items']) => PaginatedResponse['items'], +) => + queryClient.setQueryData>>(queryKey, (data) => { + if (!data) return undefined; + return { + ...data, + pages: data.pages.map((page) => ({ + ...page, + items: updater(page.items), + })), + }; + }); + +export { updatePaginatedResponse }; diff --git a/packages/pl-fe/src/reducers/conversations.ts b/packages/pl-fe/src/reducers/conversations.ts deleted file mode 100644 index da4e20791..000000000 --- a/packages/pl-fe/src/reducers/conversations.ts +++ /dev/null @@ -1,138 +0,0 @@ -import pick from 'lodash/pick'; -import { create } from 'mutative'; - -import { - CONVERSATIONS_MOUNT, - CONVERSATIONS_UNMOUNT, - CONVERSATIONS_FETCH_REQUEST, - CONVERSATIONS_FETCH_SUCCESS, - CONVERSATIONS_FETCH_FAIL, - CONVERSATIONS_UPDATE, - CONVERSATIONS_READ, - type ConversationsAction, -} from '../actions/conversations'; -import { compareDate } from '../utils/comparators'; - -import type { Conversation, PaginatedResponse } from 'pl-api'; - -interface State { - items: Array; - isLoading: boolean; - hasMore: boolean; - next: (() => Promise>) | null; - mounted: number; -} - -const initialState: State = { - items: [], - isLoading: false, - hasMore: true, - next: null, - mounted: 0, -}; - -const minifyConversation = (conversation: Conversation) => ({ - ...pick(conversation, ['id', 'unread']), - account_ids: conversation.accounts.map((a) => a.id), - last_status: conversation.last_status?.id ?? null, - last_status_created_at: conversation.last_status?.created_at ?? null, -}); - -type MinifiedConversation = ReturnType; - -const updateConversation = (state: State, item: Conversation) => { - const index = state.items.findIndex((x) => x.id === item.id); - const newItem = minifyConversation(item); - - if (index === -1) { - state.items = [newItem, ...state.items]; - } else { - state.items[index] = newItem; - } -}; - -const expandNormalizedConversations = ( - state: State, - conversations: Conversation[], - next: (() => Promise>) | null, - isLoadingRecent?: boolean, -) => { - let items = conversations.map(minifyConversation); - - if (items.length) { - let list = state.items.map((oldItem) => { - const newItemIndex = items.findIndex((x) => x.id === oldItem.id); - - if (newItemIndex === -1) { - return oldItem; - } - - const newItem = items[newItemIndex]; - items = items.filter((_, index) => index !== newItemIndex); - - return newItem; - }); - - list = list.concat(items); - - state.items = list.toSorted((a, b) => { - if (a.last_status_created_at === null || b.last_status_created_at === null) { - return -1; - } - - return compareDate(a.last_status_created_at, b.last_status_created_at); - }); - } - - state.hasMore = !next; - state.next = next; - state.isLoading = false; -}; - -const conversations = (state = initialState, action: ConversationsAction): State => { - switch (action.type) { - case CONVERSATIONS_FETCH_REQUEST: - return create(state, (draft) => { - draft.isLoading = true; - }); - case CONVERSATIONS_FETCH_FAIL: - return create(state, (draft) => { - draft.isLoading = false; - }); - case CONVERSATIONS_FETCH_SUCCESS: - return create(state, (draft) => { - expandNormalizedConversations( - draft, - action.conversations, - action.next, - action.isLoadingRecent, - ); - }); - case CONVERSATIONS_UPDATE: - return create(state, (draft) => { - updateConversation(state, action.conversation); - }); - case CONVERSATIONS_MOUNT: - return create(state, (draft) => { - draft.mounted += 1; - }); - case CONVERSATIONS_UNMOUNT: - return create(state, (draft) => { - draft.mounted -= 1; - }); - case CONVERSATIONS_READ: - return create(state, (draft) => { - state.items = state.items.map((item) => { - if (item.id === action.conversationId) { - return { ...item, unread: false }; - } - - return item; - }); - }); - default: - return state; - } -}; - -export { conversations as default }; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 789bfdc92..9dd303b14 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -7,7 +7,6 @@ import entities from '@/entity-store/reducer'; import admin from './admin'; import auth from './auth'; import compose from './compose'; -import conversations from './conversations'; import filters from './filters'; import frontendConfig from './frontend-config'; import instance from './instance'; @@ -22,7 +21,6 @@ const reducers = { admin, auth, compose, - conversations, entities, filters, frontendConfig, From 124eefd5058be70819bb0abb718bca11769387ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 18:42:32 +0100 Subject: [PATCH 033/264] nicolium: partial migration of filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/filters.ts | 103 +----------------- .../api/hooks/streaming/use-user-stream.ts | 3 +- packages/pl-fe/src/features/ui/index.tsx | 9 +- .../pl-fe/src/pages/settings/edit-filter.tsx | 77 +++++++------ packages/pl-fe/src/pages/settings/filters.tsx | 34 +++--- packages/pl-fe/src/queries/client.ts | 1 + .../pl-fe/src/queries/settings/use-filters.ts | 93 ++++++++++++++++ packages/pl-fe/src/queries/trends.ts | 2 +- 8 files changed, 156 insertions(+), 166 deletions(-) create mode 100644 packages/pl-fe/src/queries/settings/use-filters.ts diff --git a/packages/pl-fe/src/actions/filters.ts b/packages/pl-fe/src/actions/filters.ts index ebecf102a..1c416aa31 100644 --- a/packages/pl-fe/src/actions/filters.ts +++ b/packages/pl-fe/src/actions/filters.ts @@ -1,106 +1,7 @@ -import { defineMessages } from 'react-intl'; - -import toast from '@/toast'; -import { isLoggedIn } from '@/utils/auth'; - -import { getClient } from '../api'; - -import type { AppDispatch, RootState } from '@/store'; -import type { Filter, FilterContext } from 'pl-api'; +import type { Filter } from 'pl-api'; const FILTERS_FETCH_SUCCESS = 'FILTERS_FETCH_SUCCESS' as const; -const messages = defineMessages({ - added: { id: 'filters.added', defaultMessage: 'Filter added.' }, - updated: { id: 'filters.updated', defaultMessage: 'Filter updated.' }, - removed: { id: 'filters.removed', defaultMessage: 'Filter deleted.' }, -}); - -type FilterKeywords = { keyword: string; whole_word: boolean }[]; - -const fetchFilters = () => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - return getClient(getState) - .filtering.getFilters() - .then((data) => - dispatch({ - type: FILTERS_FETCH_SUCCESS, - filters: data, - }), - ) - .catch((error) => ({ - error, - })); -}; - -const fetchFilter = (filterId: string) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState).filtering.getFilter(filterId); - -const createFilter = - ( - title: string, - expires_in: number | undefined, - context: Array, - filter_action: Filter['filter_action'], - keywords_attributes: FilterKeywords, - ) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState) - .filtering.createFilter({ - title, - context, - filter_action, - expires_in, - keywords_attributes, - }) - .then((response) => { - toast.success(messages.added); - - return response; - }); - -const updateFilter = - ( - filterId: string, - title: string, - expires_in: number | undefined, - context: Array, - filter_action: Filter['filter_action'], - keywords_attributes: FilterKeywords, - ) => - (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState) - .filtering.updateFilter(filterId, { - title, - context, - filter_action, - expires_in, - keywords_attributes, - }) - .then((response) => { - toast.success(messages.updated); - - return response; - }); - -const deleteFilter = (filterId: string) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState) - .filtering.deleteFilter(filterId) - .then((response) => { - toast.success(messages.removed); - - return response; - }); - type FiltersAction = { type: typeof FILTERS_FETCH_SUCCESS; filters: Array }; -export { - FILTERS_FETCH_SUCCESS, - fetchFilters, - fetchFilter, - createFilter, - updateFilter, - deleteFilter, - type FiltersAction, -}; +export { FILTERS_FETCH_SUCCESS, type FiltersAction }; diff --git a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts index a8d120098..972ea4dbc 100644 --- a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts @@ -1,6 +1,5 @@ import { useCallback } from 'react'; -import { fetchFilters } from '@/actions/filters'; import { MARKER_FETCH_SUCCESS } from '@/actions/markers'; import { updateNotificationsQueue } from '@/actions/notifications'; import { getLocale } from '@/actions/settings'; @@ -134,7 +133,7 @@ const useUserStream = () => { updateConversations(event.payload); break; case 'filters_changed': - dispatch(fetchFilters()); + queryClient.invalidateQueries({ queryKey: ['filters'] }); break; case 'chat_update': dispatch((_dispatch, getState) => { diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index dd352411f..dc49bd4ce 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -4,7 +4,6 @@ import React, { Suspense, useEffect, useRef } from 'react'; import { Toaster } from 'react-hot-toast'; import { fetchConfig } from '@/actions/admin'; -import { fetchFilters } from '@/actions/filters'; import { fetchMarker } from '@/actions/markers'; import { expandNotifications } from '@/actions/notifications'; import { register as registerPushNotifications } from '@/actions/push-notifications/registerer'; @@ -23,6 +22,7 @@ import { useOwnAccount } from '@/hooks/use-own-account'; import { prefetchFollowRequests } from '@/queries/accounts/use-follow-requests'; import { queryClient } from '@/queries/client'; import { prefetchCustomEmojis } from '@/queries/instance/use-custom-emojis'; +import { useFilters } from '@/queries/settings/use-filters'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useSettings } from '@/stores/settings'; import { useShoutboxSubscription } from '@/stores/shoutbox'; @@ -38,11 +38,11 @@ import { DropdownNavigation, StatusHoverCard, } from './util/async-components'; -import GlobalHotkeys from './util/global-hotkeys'; // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '@/components/status'; +import GlobalHotkeys from './util/global-hotkeys'; const UI: React.FC = React.memo(() => { const navigate = useNavigate(); @@ -60,6 +60,7 @@ const UI: React.FC = React.memo(() => { const standalone = useAppSelector(isStandalone); useShoutboxSubscription(); + useFilters(); const { isDragging } = useDraggedFiles(node); @@ -100,10 +101,6 @@ const UI: React.FC = React.memo(() => { dispatch(fetchConfig()); } - if (features.filters || features.filtersV2) { - setTimeout(() => dispatch(fetchFilters()), 500); - } - if (account.locked) { setTimeout(() => prefetchFollowRequests(client), 700); } diff --git a/packages/pl-fe/src/pages/settings/edit-filter.tsx b/packages/pl-fe/src/pages/settings/edit-filter.tsx index 54c2d2873..e654f7c95 100644 --- a/packages/pl-fe/src/pages/settings/edit-filter.tsx +++ b/packages/pl-fe/src/pages/settings/edit-filter.tsx @@ -3,7 +3,6 @@ import { Filter, type FilterContext } from 'pl-api'; import React, { useEffect, useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { createFilter, fetchFilter, updateFilter } from '@/actions/filters'; import List, { ListItem } from '@/components/list'; import MissingIndicator from '@/components/missing-indicator'; import Button from '@/components/ui/button'; @@ -20,8 +19,8 @@ import Text from '@/components/ui/text'; import Toggle from '@/components/ui/toggle'; import { SelectDropdown } from '@/features/forms'; import { editFilterRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; +import { useCreateFilter, useFilter, useUpdateFilter } from '@/queries/settings/use-filters'; import toast from '@/toast'; import type { StreamfieldComponent } from '@/components/ui/streamfield'; @@ -76,7 +75,7 @@ const messages = defineMessages({ }, add_new: { id: 'column.filters.add_new', defaultMessage: 'Add new filter' }, edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' }, - create_error: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' }, + createError: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' }, expiration_never: { id: 'column.filters.expiration.never', defaultMessage: 'Never' }, expiration_1800: { id: 'column.filters.expiration.1800', defaultMessage: '30 minutes' }, expiration_3600: { id: 'column.filters.expiration.3600', defaultMessage: '1 hour' }, @@ -123,11 +122,15 @@ const EditFilterPage: React.FC = () => { const intl = useIntl(); const navigate = useNavigate(); - const dispatch = useAppDispatch(); const features = useFeatures(); - const [loading, setLoading] = useState(false); - const [notFound, setNotFound] = useState(false); + const { + data: filter, + isFetching: isFetchingFilter, + isError: notFound, + } = useFilter(filterId !== 'new' ? filterId : undefined); + const { mutate: createFilter, isPending: isCreating } = useCreateFilter(); + const { mutate: updateFilter, isPending: isUpdating } = useUpdateFilter(filterId); const [title, setTitle] = useState(''); const [expiresIn, setExpiresIn] = useState(); @@ -176,17 +179,23 @@ const EditFilterPage: React.FC = () => { context.push('account'); } - dispatch( - filterId !== 'new' - ? updateFilter(filterId, title, expiresIn, context, filterAction, keywords) - : createFilter(title, expiresIn, context, filterAction, keywords), - ) - .then(() => { - navigate({ to: '/filters' }); - }) - .catch(() => { - toast.error(intl.formatMessage(messages.create_error)); - }); + (filterId !== 'new' ? updateFilter : createFilter)( + { + title, + expires_in: expiresIn, + context, + filter_action: filterAction, + keywords_attributes: keywords, + }, + { + onSuccess: () => { + navigate({ to: '/filters' }); + }, + onError: () => { + toast.error(intl.formatMessage(messages.createError)); + }, + }, + ); }; const handleChangeKeyword = (keywords: { keyword: string; whole_word: boolean }[]) => { @@ -206,25 +215,17 @@ const EditFilterPage: React.FC = () => { }; useEffect(() => { - if (filterId !== 'new') { - setLoading(true); - dispatch(fetchFilter(filterId))?.then((filter) => { - if (filter) { - setTitle(filter.title); - setHomeTimeline(filter.context.includes('home')); - setPublicTimeline(filter.context.includes('public')); - setNotifications(filter.context.includes('notifications')); - setConversations(filter.context.includes('thread')); - setAccounts(filter.context.includes('account')); - setFilterAction(filter.filter_action); - setKeywords(filter.keywords); - } else { - setNotFound(true); - } - setLoading(false); - }); + if (filter) { + setTitle(filter.title); + setHomeTimeline(filter.context.includes('home')); + setPublicTimeline(filter.context.includes('public')); + setNotifications(filter.context.includes('notifications')); + setConversations(filter.context.includes('thread')); + setAccounts(filter.context.includes('account')); + setFilterAction(filter.filter_action); + setKeywords(filter.keywords); } - }, [filterId]); + }, [isFetchingFilter]); if (notFound) return ; @@ -363,7 +364,11 @@ const EditFilterPage: React.FC = () => { {features.filtersV2 && keywordsField} - diff --git a/packages/pl-fe/src/pages/settings/filters.tsx b/packages/pl-fe/src/pages/settings/filters.tsx index 264d0e862..5fe517f5e 100644 --- a/packages/pl-fe/src/pages/settings/filters.tsx +++ b/packages/pl-fe/src/pages/settings/filters.tsx @@ -1,7 +1,6 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { fetchFilters, deleteFilter } from '@/actions/filters'; import RelativeTimestamp from '@/components/relative-timestamp'; import ScrollableList from '@/components/scrollable-list'; import Button from '@/components/ui/button'; @@ -9,26 +8,25 @@ import Column from '@/components/ui/column'; import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; +import { useDeleteFilter, useFilters } from '@/queries/settings/use-filters'; import toast from '@/toast'; const messages = defineMessages({ heading: { id: 'column.filters', defaultMessage: 'Muted words' }, - home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' }, - public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' }, + homeTimeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' }, + publicTimeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' }, notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' }, conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' }, accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' }, - delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' }, + deleteError: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' }, edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' }, delete: { id: 'column.filters.delete', defaultMessage: 'Delete' }, }); const contexts = { - home: messages.home_timeline, - public: messages.public_timeline, + home: messages.homeTimeline, + public: messages.publicTimeline, notifications: messages.notifications, thread: messages.conversations, account: messages.accounts, @@ -36,23 +34,19 @@ const contexts = { const FiltersPage = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); const { filtersV2 } = useFeatures(); - const filters = useAppSelector((state) => state.filters); + const { data: filters = [] } = useFilters(); + const { mutate: deleteFilter } = useDeleteFilter(); const handleFilterDelete = (id: string) => () => { - dispatch(deleteFilter(id)) - .then(() => dispatch(fetchFilters())) - .catch(() => { - toast.error(intl.formatMessage(messages.delete_error)); - }); + deleteFilter(id, { + onError: () => { + toast.error(intl.formatMessage(messages.deleteError)); + }, + }); }; - useEffect(() => { - dispatch(fetchFilters()); - }, []); - const emptyMessage = ( ); diff --git a/packages/pl-fe/src/queries/client.ts b/packages/pl-fe/src/queries/client.ts index 9846dbd32..a3e7a6bc2 100644 --- a/packages/pl-fe/src/queries/client.ts +++ b/packages/pl-fe/src/queries/client.ts @@ -4,6 +4,7 @@ const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, + refetchOnReconnect: false, staleTime: 60000, // 1 minute gcTime: Infinity, retry: false, diff --git a/packages/pl-fe/src/queries/settings/use-filters.ts b/packages/pl-fe/src/queries/settings/use-filters.ts new file mode 100644 index 000000000..13de4f84b --- /dev/null +++ b/packages/pl-fe/src/queries/settings/use-filters.ts @@ -0,0 +1,93 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { type FiltersAction, FILTERS_FETCH_SUCCESS } from '@/actions/filters'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; +import { useFeatures } from '@/hooks/use-features'; + +import type { CreateFilterParams, Filter, UpdateFilterParams } from 'pl-api'; + +const useFilters = () => { + const client = useClient(); + const dispatch = useAppDispatch(); + const features = useFeatures(); + + return useQuery({ + queryKey: ['filters'], + queryFn: async () => { + const response = await client.filtering.getFilters(); + + dispatch({ + type: FILTERS_FETCH_SUCCESS, + filters: response, + }); + + return response; + }, + enabled: features.filters || features.filtersV2, + staleTime: 30 * 60 * 1000, + }); +}; + +const useFilter = (filterId?: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useQuery({ + queryKey: ['filters', filterId], + queryFn: () => { + if (!filterId) return undefined; + return client.filtering.getFilter(filterId); + }, + enabled: !!filterId, + placeholderData: () => { + queryClient + .getQueryData>(['filters']) + ?.find((filter) => filter.id === filterId); + }, + }); +}; + +const useCreateFilter = () => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['filters', 'create'], + mutationFn: (data: CreateFilterParams) => client.filtering.createFilter(data), + onSettled: (data) => { + queryClient.invalidateQueries({ queryKey: ['filters'] }); + if (data) queryClient.setQueryData(['filters', data.id], data); + }, + }); +}; + +const useUpdateFilter = (filterId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['filters', filterId, 'update'], + mutationFn: (data: UpdateFilterParams) => client.filtering.updateFilter(filterId, data), + onSettled: (data) => { + queryClient.invalidateQueries({ queryKey: ['filters'] }); + if (data) queryClient.setQueryData(['filters', filterId], data); + }, + }); +}; + +const useDeleteFilter = () => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['filters', 'delete'], + mutationFn: (filterId: string) => client.filtering.deleteFilter(filterId), + onSettled: (_, __, filterId) => { + queryClient.invalidateQueries({ queryKey: ['filters'] }); + queryClient.invalidateQueries({ queryKey: ['filters', filterId] }); + }, + }); +}; + +export { useFilters, useFilter, useCreateFilter, useUpdateFilter, useDeleteFilter }; diff --git a/packages/pl-fe/src/queries/trends.ts b/packages/pl-fe/src/queries/trends.ts index d405fa272..1d1f2bdf9 100644 --- a/packages/pl-fe/src/queries/trends.ts +++ b/packages/pl-fe/src/queries/trends.ts @@ -15,7 +15,7 @@ const useTrends = () => { queryKey: ['trends', 'tags'], queryFn: () => client.trends.getTrendingTags(), placeholderData: [], - staleTime: 600000, // 10 minutes + staleTime: 10 * 60 * 1000, // 10 minutes enabled: isLoggedIn && features.trends, }); }; From 060f959d07a0859337c308af373d511cab11f518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 19:22:49 +0100 Subject: [PATCH 034/264] nicolium: i love committing code that doesn't compile but it passes formatting and linting so at least it's not ugly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/queries/settings/use-filters.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/pl-fe/src/queries/settings/use-filters.ts b/packages/pl-fe/src/queries/settings/use-filters.ts index 13de4f84b..ae4ae11eb 100644 --- a/packages/pl-fe/src/queries/settings/use-filters.ts +++ b/packages/pl-fe/src/queries/settings/use-filters.ts @@ -40,11 +40,10 @@ const useFilter = (filterId?: string) => { return client.filtering.getFilter(filterId); }, enabled: !!filterId, - placeholderData: () => { + placeholderData: () => queryClient .getQueryData>(['filters']) - ?.find((filter) => filter.id === filterId); - }, + ?.find((filter) => filter.id === filterId), }); }; From f8e0d2d47c3b1e7c2babbc7c661731bbe3292d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 21:24:13 +0100 Subject: [PATCH 035/264] nicolium: use helpers for minification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../conversations/use-conversations.ts | 92 +++++++------------ .../pl-fe/src/queries/utils/minify-list.ts | 30 ++++++ 2 files changed, 61 insertions(+), 61 deletions(-) diff --git a/packages/pl-fe/src/queries/conversations/use-conversations.ts b/packages/pl-fe/src/queries/conversations/use-conversations.ts index 11a4c85e1..3650a1c47 100644 --- a/packages/pl-fe/src/queries/conversations/use-conversations.ts +++ b/packages/pl-fe/src/queries/conversations/use-conversations.ts @@ -14,28 +14,15 @@ import { store } from '@/store'; import { compareDate } from '@/utils/comparators'; import { queryClient } from '../client'; +import { + minifyConversation, + minifyConversationList, + type MinifiedConversation, +} from '../utils/minify-list'; import { updatePaginatedResponse } from '../utils/update-paginated-response'; import type { Conversation, PaginatedResponse } from 'pl-api'; -type MinifiedConversation = { - id: string; - unread: boolean; - account_ids: string[]; - last_status: string | null; - last_status_created_at: string | null; -}; - -type MinifiedConversationPage = PaginatedResponse; - -const minifyConversation = (conversation: Conversation): MinifiedConversation => ({ - id: conversation.id, - unread: conversation.unread, - account_ids: conversation.accounts.map((account) => account.id), - last_status: conversation.last_status?.id ?? null, - last_status_created_at: conversation.last_status?.created_at ?? null, -}); - const sortConversations = (items: MinifiedConversation[]) => items.toSorted((a, b) => { if (a.last_status_created_at === null || b.last_status_created_at === null) { @@ -54,48 +41,34 @@ const importConversationEntities = (conversations: Conversation[]) => { ); }; -const minifyConversationPage = ( - response: PaginatedResponse, -): MinifiedConversationPage => { - importConversationEntities(response.items); - - return { - ...response, - previous: response.previous - ? () => response.previous!().then((page) => minifyConversationPage(page)) - : null, - next: response.next - ? () => response.next!().then((page) => minifyConversationPage(page)) - : null, - items: response.items.map(minifyConversation), - }; -}; - const updateConversations = (conversation: Conversation) => { importConversationEntities([conversation]); - queryClient.setQueryData>(['conversations'], (data) => { - if (!data || !data.pages.length) return data; + queryClient.setQueryData>>( + ['conversations'], + (data) => { + if (!data || !data.pages.length) return data; - return create(data, (draft) => { - const updatedConversation = minifyConversation(conversation); + return create(data, (draft) => { + const updatedConversation = minifyConversation(conversation); - let found = false; + let found = false; - for (const page of draft.pages) { - const index = page.items.findIndex((item) => item.id === updatedConversation.id); - if (index !== -1) { - page.items[index] = updatedConversation; - found = true; - break; + for (const page of draft.pages) { + const index = page.items.findIndex((item) => item.id === updatedConversation.id); + if (index !== -1) { + page.items[index] = updatedConversation; + found = true; + break; + } } - } - if (!found) { - draft.pages[0].items.unshift(updatedConversation); - } - }); - }); + if (!found) { + draft.pages[0].items.unshift(updatedConversation); + } + }); + }, + ); }; const useConversations = () => { @@ -110,14 +83,11 @@ const useConversations = () => { } const response = await client.timelines.getConversations(); - return minifyConversationPage(response); + return minifyConversationList(response); }, initialPageParam: { - previous: null, - next: null, - items: [], - partial: false, - } as MinifiedConversationPage, + next: null as (() => Promise>) | null, + } as PaginatedResponse, getNextPageParam: (page) => (page.next ? page : undefined), enabled: isLoggedIn, }); @@ -140,9 +110,9 @@ const useMarkConversationRead = (conversationId: string) => { onMutate: async () => { await queryClient.cancelQueries({ queryKey: ['conversations'] }); - const previous = queryClient.getQueryData>([ - 'conversations', - ]); + const previous = queryClient.getQueryData< + InfiniteData> + >(['conversations']); updatePaginatedResponse(['conversations'], (items) => items.map((item) => (item.id === conversationId ? { ...item, unread: false } : item)), diff --git a/packages/pl-fe/src/queries/utils/minify-list.ts b/packages/pl-fe/src/queries/utils/minify-list.ts index 2c9462b9f..8b1407100 100644 --- a/packages/pl-fe/src/queries/utils/minify-list.ts +++ b/packages/pl-fe/src/queries/utils/minify-list.ts @@ -8,6 +8,7 @@ import type { AdminAccount, AdminReport, BlockedAccount, + Conversation, Group, MutedAccount, PaginatedResponse, @@ -82,6 +83,32 @@ const minifyGroupList = (response: PaginatedResponse): PaginatedResponse< }, ); +type MinifiedConversation = { + id: string; + unread: boolean; + account_ids: string[]; + last_status: string | null; + last_status_created_at: string | null; +}; + +const minifyConversation = (conversation: Conversation): MinifiedConversation => ({ + id: conversation.id, + unread: conversation.unread, + account_ids: conversation.accounts.map((account) => account.id), + last_status: conversation.last_status?.id ?? null, + last_status_created_at: conversation.last_status?.created_at ?? null, +}); + +const minifyConversationList = (response: PaginatedResponse) => + minifyList(response, minifyConversation, (conversations) => { + store.dispatch( + importEntities({ + accounts: conversations.flatMap((conversation) => conversation.accounts), + statuses: conversations.map((conversation) => conversation.last_status), + }) as any, + ); + }); + const minifyAdminAccount = ({ account, ...adminAccount }: AdminAccount) => { store.dispatch(importEntities({ accounts: [account] }) as any); queryClient.setQueryData(['admin', 'accounts', adminAccount.id], adminAccount); @@ -159,8 +186,11 @@ export { minifyMutedAccountList, minifyStatusList, minifyGroupList, + minifyConversation, + minifyConversationList, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, minifyAdminReportList, + type MinifiedConversation, }; From e3aaa580b5b651a069fa9b7faf72b83561dd8529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 21:26:20 +0100 Subject: [PATCH 036/264] nicolium: infer type from minifier output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/queries/utils/minify-list.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/pl-fe/src/queries/utils/minify-list.ts b/packages/pl-fe/src/queries/utils/minify-list.ts index 8b1407100..3d66a8934 100644 --- a/packages/pl-fe/src/queries/utils/minify-list.ts +++ b/packages/pl-fe/src/queries/utils/minify-list.ts @@ -83,15 +83,7 @@ const minifyGroupList = (response: PaginatedResponse): PaginatedResponse< }, ); -type MinifiedConversation = { - id: string; - unread: boolean; - account_ids: string[]; - last_status: string | null; - last_status_created_at: string | null; -}; - -const minifyConversation = (conversation: Conversation): MinifiedConversation => ({ +const minifyConversation = (conversation: Conversation) => ({ id: conversation.id, unread: conversation.unread, account_ids: conversation.accounts.map((account) => account.id), @@ -99,6 +91,8 @@ const minifyConversation = (conversation: Conversation): MinifiedConversation => last_status_created_at: conversation.last_status?.created_at ?? null, }); +type MinifiedConversation = ReturnType; + const minifyConversationList = (response: PaginatedResponse) => minifyList(response, minifyConversation, (conversations) => { store.dispatch( From 6ceee73b601c1dde6193a6ffd09cf50b00a1f848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 22:51:10 +0100 Subject: [PATCH 037/264] nicolium: migrate notifications to tanstack/react-query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/notifications.ts | 349 ----------------- .../api/hooks/streaming/use-user-stream.ts | 8 +- packages/pl-fe/src/columns/notifications.tsx | 176 +++++---- packages/pl-fe/src/components/helmet.tsx | 12 +- .../src/components/scroll-top-button.tsx | 2 +- .../src/components/sidebar-navigation.tsx | 4 +- .../pl-fe/src/components/thumb-navigation.tsx | 3 +- .../notifications/components/notification.tsx | 7 +- packages/pl-fe/src/features/ui/index.tsx | 12 +- .../notifications/use-notifications.ts | 356 ++++++++++++++++++ .../make-paginated-response-query-options.ts | 48 ++- .../utils/make-paginated-response-query.ts | 51 ++- .../pl-fe/src/queries/utils/minify-list.ts | 44 ++- packages/pl-fe/src/reducers/index.ts | 2 - packages/pl-fe/src/reducers/notifications.ts | 187 --------- packages/pl-fe/src/schemas/pl-fe/settings.ts | 5 +- 16 files changed, 581 insertions(+), 685 deletions(-) delete mode 100644 packages/pl-fe/src/actions/notifications.ts create mode 100644 packages/pl-fe/src/queries/notifications/use-notifications.ts delete mode 100644 packages/pl-fe/src/reducers/notifications.ts diff --git a/packages/pl-fe/src/actions/notifications.ts b/packages/pl-fe/src/actions/notifications.ts deleted file mode 100644 index 2346b0f76..000000000 --- a/packages/pl-fe/src/actions/notifications.ts +++ /dev/null @@ -1,349 +0,0 @@ -import IntlMessageFormat from 'intl-messageformat'; -import 'intl-pluralrules'; -import { defineMessages } from 'react-intl'; - -import { getClient } from '@/api'; -import { getNotificationStatus } from '@/features/notifications/components/notification'; -import { normalizeNotification } from '@/normalizers/notification'; -import { appendFollowRequest } from '@/queries/accounts/use-follow-requests'; -import { getFilters, regexFromFilters } from '@/selectors'; -import { useSettingsStore } from '@/stores/settings'; -import { isLoggedIn } from '@/utils/auth'; -import { compareId } from '@/utils/comparators'; -import { unescapeHTML } from '@/utils/html'; -import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from '@/utils/notification'; -import { joinPublicPath } from '@/utils/static'; - -import { fetchRelationships } from './accounts'; -import { importEntities } from './importer'; -import { saveMarker } from './markers'; -import { saveSettings } from './settings'; - -import type { AppDispatch, RootState } from '@/store'; -import type { - Notification as BaseNotification, - GetGroupedNotificationsParams, - GroupedNotificationsResults, - NotificationGroup, - PaginatedResponse, -} from 'pl-api'; - -const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE' as const; -const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP' as const; - -const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST' as const; -const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS' as const; -const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL' as const; - -const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET' as const; - -const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP' as const; - -const FILTER_TYPES = { - all: undefined, - mention: ['mention', 'quote'], - favourite: ['favourite', 'emoji_reaction', 'reaction'], - reblog: ['reblog'], - poll: ['poll'], - status: ['status'], - follow: ['follow', 'follow_request'], - events: ['event_reminder', 'participation_request', 'participation_accepted'], -}; - -type FilterType = keyof typeof FILTER_TYPES; - -defineMessages({ - mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, -}); - -const fetchRelatedRelationships = ( - dispatch: AppDispatch, - notifications: Array, -) => { - const accountIds = notifications - .filter((item) => item.type === 'follow') - .map((item) => item.sample_account_ids) - .flat(); - - if (accountIds.length > 0) { - dispatch(fetchRelationships(accountIds)); - } -}; - -interface NotificationsUpdateAction { - type: typeof NOTIFICATIONS_UPDATE; - notification: NotificationGroup; -} - -const updateNotifications = (notification: BaseNotification) => (dispatch: AppDispatch) => { - const selectedFilter = useSettingsStore.getState().settings.notifications.quickFilter.active; - const showInColumn = - selectedFilter === 'all' - ? true - : (FILTER_TYPES[selectedFilter as FilterType] ?? [notification.type]).includes( - notification.type, - ); - - dispatch( - importEntities({ - accounts: [ - notification.account, - notification.type === 'move' ? notification.target : undefined, - ], - statuses: [getNotificationStatus(notification) as any], - }), - ); - - if (showInColumn) { - const normalizedNotification = normalizeNotification(notification); - - if (normalizedNotification.type === 'follow_request') { - normalizedNotification.sample_account_ids.forEach(appendFollowRequest); - } - - dispatch({ - type: NOTIFICATIONS_UPDATE, - notification: normalizedNotification, - }); - - fetchRelatedRelationships(dispatch, [normalizedNotification]); - } -}; - -interface NotificationsUpdateNoopAction { - type: typeof NOTIFICATIONS_UPDATE_NOOP; - meta: { sound: 'boop' }; -} - -const updateNotificationsQueue = - (notification: BaseNotification, intlMessages: Record, intlLocale: string) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!notification.type) return; // drop invalid notifications - if (notification.type === 'chat_mention') return; // Drop chat notifications, handle them per-chat - - const filters = getFilters(getState(), { contextType: 'notifications' }); - const playSound = useSettingsStore.getState().settings.notifications.sounds[notification.type]; - - const status = getNotificationStatus(notification); - - let filtered: boolean | null = false; - - if (notification.type === 'mention' || notification.type === 'status') { - const regex = regexFromFilters(filters); - const searchIndex = - notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); - filtered = regex && regex.test(searchIndex); - } - - // Desktop notifications - try { - const isNotificationsEnabled = window.Notification?.permission === 'granted'; - - if (!filtered && isNotificationsEnabled) { - const title = new IntlMessageFormat( - intlMessages[`notification.${notification.type}`], - intlLocale, - ).format({ - name: - notification.account.display_name.length > 0 - ? notification.account.display_name - : notification.account.username, - }) as string; - const body = - status && status.spoiler_text.length > 0 - ? status.spoiler_text - : unescapeHTML(status ? status.content : ''); - - navigator.serviceWorker.ready - .then((serviceWorkerRegistration) => { - serviceWorkerRegistration - .showNotification(title, { - body, - icon: notification.account.avatar, - tag: notification.id, - data: { - url: joinPublicPath('/notifications'), - }, - }) - .catch(console.error); - }) - .catch(console.error); - } - } catch (e) { - console.warn(e); - } - - if (playSound && !filtered) { - dispatch({ - type: NOTIFICATIONS_UPDATE_NOOP, - meta: { sound: 'boop' }, - }); - } - - dispatch(updateNotifications(notification)); - }; - -const excludeTypesFromFilter = (filters: string[]) => - NOTIFICATION_TYPES.filter((item) => !filters.includes(item)); - -const noOp = () => - new Promise((f) => { - f(undefined); - }); - -let abortExpandNotifications = new AbortController(); - -const expandNotifications = - ({ maxId }: Record = {}, done: () => any = noOp, abort?: boolean) => - async (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return dispatch(noOp); - const state = getState(); - - const features = state.auth.client.features; - const activeFilter = useSettingsStore.getState().settings.notifications.quickFilter - .active as FilterType; - const notifications = state.notifications; - - if (notifications.isLoading) { - if (abort) { - abortExpandNotifications.abort(); - abortExpandNotifications = new AbortController(); - } else { - done(); - return dispatch(noOp); - } - } - - const params: GetGroupedNotificationsParams = { - max_id: maxId, - }; - - if (activeFilter === 'all') { - if (features.notificationsIncludeTypes) { - params.types = NOTIFICATION_TYPES.filter((type) => !EXCLUDE_TYPES.includes(type as any)); - } else { - params.exclude_types = [...EXCLUDE_TYPES]; - } - } else { - const filtered = FILTER_TYPES[activeFilter] || [activeFilter]; - if (features.notificationsIncludeTypes) { - params.types = filtered; - } else { - params.exclude_types = excludeTypesFromFilter(filtered); - } - } - - dispatch(expandNotificationsRequest()); - - try { - const { - items: { accounts, statuses, notification_groups }, - next, - } = await getClient(state).groupedNotifications.getGroupedNotifications(params, { - signal: abortExpandNotifications.signal, - }); - - dispatch( - importEntities({ - accounts, - statuses, - }), - ); - - dispatch(expandNotificationsSuccess(notification_groups, next)); - fetchRelatedRelationships(dispatch, notification_groups); - done(); - } catch (error) { - dispatch(expandNotificationsFail(error)); - done(); - } - }; - -const expandNotificationsRequest = () => ({ type: NOTIFICATIONS_EXPAND_REQUEST }); - -const expandNotificationsSuccess = ( - notifications: Array, - next: (() => Promise>) | null, -) => ({ - type: NOTIFICATIONS_EXPAND_SUCCESS, - notifications, - next, -}); - -const expandNotificationsFail = (error: unknown) => ({ - type: NOTIFICATIONS_EXPAND_FAIL, - error, -}); - -interface NotificationsScrollTopAction { - type: typeof NOTIFICATIONS_SCROLL_TOP; - top: boolean; -} - -const scrollTopNotifications = (top: boolean) => (dispatch: AppDispatch) => { - dispatch(markReadNotifications()); - return dispatch({ - type: NOTIFICATIONS_SCROLL_TOP, - top, - }); -}; - -interface SetFilterAction { - type: typeof NOTIFICATIONS_FILTER_SET; -} - -const setFilter = (filterType: FilterType, abort?: boolean) => (dispatch: AppDispatch) => { - const settingsStore = useSettingsStore.getState(); - const activeFilter = settingsStore.settings.notifications.quickFilter.active as FilterType; - - settingsStore.actions.changeSetting(['notifications', 'quickFilter', 'active'], filterType); - - dispatch(expandNotifications(undefined, undefined, abort)); - if (activeFilter !== filterType) dispatch(saveSettings()); - - return dispatch({ type: NOTIFICATIONS_FILTER_SET }); -}; - -const markReadNotifications = () => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - - const state = getState(); - const topNotificationId = state.notifications.items[0]?.page_max_id; - const lastReadId = state.notifications.lastRead; - - if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { - const marker = { - notifications: { - last_read_id: topNotificationId, - }, - }; - - dispatch(saveMarker(marker)); - } -}; - -type NotificationsAction = - | NotificationsUpdateAction - | NotificationsUpdateNoopAction - | ReturnType - | ReturnType - | ReturnType - | NotificationsScrollTopAction - | SetFilterAction; - -export { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - type FilterType, - updateNotifications, - updateNotificationsQueue, - expandNotifications, - scrollTopNotifications, - setFilter, - markReadNotifications, - type NotificationsAction, -}; diff --git a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts index 972ea4dbc..7a7d61c39 100644 --- a/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts +++ b/packages/pl-fe/src/api/hooks/streaming/use-user-stream.ts @@ -1,7 +1,5 @@ import { useCallback } from 'react'; -import { MARKER_FETCH_SUCCESS } from '@/actions/markers'; -import { updateNotificationsQueue } from '@/actions/notifications'; import { getLocale } from '@/actions/settings'; import { updateStatus } from '@/actions/statuses'; import { deleteFromTimelines, processTimelineUpdate } from '@/actions/timelines'; @@ -11,6 +9,7 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import messages from '@/messages'; import { queryClient } from '@/queries/client'; import { updateConversations } from '@/queries/conversations/use-conversations'; +import { useProcessStreamNotification } from '@/queries/notifications/use-notifications'; import { useSettings } from '@/stores/settings'; import { getUnreadChatsCount, updateChatListItem } from '@/utils/chats'; import { play, soundCache } from '@/utils/sounds'; @@ -108,6 +107,7 @@ const useUserStream = () => { const dispatch = useAppDispatch(); const statContext = useStatContext(); const settings = useSettings(); + const processStreamNotification = useProcessStreamNotification(); const listener = useCallback((event: StreamingEvent) => { switch (event.event) { @@ -123,7 +123,7 @@ const useUserStream = () => { case 'notification': messages[getLocale()]() .then((messages) => { - dispatch(updateNotificationsQueue(event.payload, messages, getLocale())); + processStreamNotification(event.payload, messages, getLocale()); }) .catch((error) => { console.error(error); @@ -167,7 +167,7 @@ const useUserStream = () => { deleteAnnouncement(event.payload); break; case 'marker': - dispatch({ type: MARKER_FETCH_SUCCESS, marker: event.payload }); + queryClient.setQueryData(['markers', 'notifications'], event.payload ?? null); break; } }, []); diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index a515f5b4c..6dc593a7b 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -1,17 +1,12 @@ +import { InfiniteData, useQueryClient } from '@tanstack/react-query'; import clsx from 'clsx'; import debounce from 'lodash/debounce'; -import React, { useCallback, useEffect, useRef, useState } from 'react'; -import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { createSelector } from 'reselect'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import '@/styles/new/notifications.scss'; -import { - type FilterType, - expandNotifications, - markReadNotifications, - scrollTopNotifications, - setFilter, -} from '@/actions/notifications'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import { saveSettings } from '@/actions/settings'; import PullToRefresh from '@/components/pull-to-refresh'; import ScrollTopButton from '@/components/scroll-top-button'; import ScrollableList from '@/components/scrollable-list'; @@ -21,13 +16,16 @@ import Tabs from '@/components/ui/tabs'; import Notification from '@/features/notifications/components/notification'; import PlaceholderNotification from '@/features/placeholder/components/placeholder-notification'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; -import { useSettings } from '@/stores/settings'; +import { + type FilterType, + useMarkNotificationsReadMutation, + useNotifications, +} from '@/queries/notifications/use-notifications'; +import { useSettings, useSettingsStoreActions } from '@/stores/settings'; import { selectChild } from '@/utils/scroll-utils'; import type { Item } from '@/components/ui/tabs'; -import type { RootState } from '@/store'; import type { VirtuosoHandle } from 'react-virtuoso'; const messages = defineMessages({ @@ -58,17 +56,15 @@ const FilterBar = () => { const intl = useIntl(); const dispatch = useAppDispatch(); const settings = useSettings(); + const { changeSetting } = useSettingsStoreActions(); const features = useFeatures(); const selectedFilter = settings.notifications.quickFilter.active; const advancedMode = settings.notifications.quickFilter.advanced; - const onClick = (notificationType: FilterType) => () => { - try { - dispatch(setFilter(notificationType, true)); - } catch (e) { - console.error(e); - } + const onClick = (filterType: FilterType) => () => { + changeSetting(['notifications', 'quickFilter', 'active'], filterType); + dispatch(saveSettings()); }; const items: Item[] = [ @@ -174,21 +170,47 @@ const FilterBar = () => { return ; }; -const getNotifications = createSelector( - [ - (state: RootState) => state.notifications.items, - (_, topNotification?: string) => topNotification, - ], - (notifications, topNotificationId) => { - if (topNotificationId) { - const queuedNotificationCount = notifications.findIndex( - (notification) => notification.most_recent_notification_id <= topNotificationId, +interface INotificationsColumn { + multiColumn?: boolean; +} + +const NotificationsColumn: React.FC = ({ multiColumn }) => { + const features = useFeatures(); + const settings = useSettings(); + const { mutate: markNotificationsRead } = useMarkNotificationsReadMutation(); + const queryClient = useQueryClient(); + + const showFilterBar = + (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && + settings.notifications.quickFilter.show; + const activeFilter = settings.notifications.quickFilter.active; + const { + data: notifications = [], + isLoading, + isFetching, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + refetch, + } = useNotifications(activeFilter); + + const [topNotification, setTopNotification] = useState(); + const { queuedNotificationCount, displayedNotifications } = useMemo(() => { + if (topNotification) { + const cutoffIndex = notifications.findIndex( + (notification) => notification.most_recent_notification_id <= topNotification, ); - const displayedNotifications = notifications.slice(queuedNotificationCount); + + if (cutoffIndex === -1) { + return { + queuedNotificationCount: 0, + displayedNotifications: notifications, + }; + } return { - queuedNotificationCount, - displayedNotifications, + queuedNotificationCount: cutoffIndex, + displayedNotifications: notifications.slice(cutoffIndex), }; } @@ -196,67 +218,35 @@ const getNotifications = createSelector( queuedNotificationCount: 0, displayedNotifications: notifications, }; - }, -); - -interface INotificationsColumn { - multiColumn?: boolean; -} - -const NotificationsColumn: React.FC = ({ multiColumn }) => { - const dispatch = useAppDispatch(); - const features = useFeatures(); - const settings = useSettings(); - - const showFilterBar = - (features.notificationsExcludeTypes || features.notificationsIncludeTypes) && - settings.notifications.quickFilter.show; - const activeFilter = settings.notifications.quickFilter.active; - const [topNotification, setTopNotification] = useState(); - const { queuedNotificationCount, displayedNotifications } = useAppSelector((state) => - getNotifications(state, topNotification), - ); - const isLoading = useAppSelector((state) => state.notifications.isLoading); - // const isUnread = useAppSelector(state => state.notifications.unread > 0); - const hasMore = useAppSelector((state) => state.notifications.hasMore); + }, [notifications, topNotification]); + const hasMore = hasNextPage ?? false; const node = useRef(null); const scrollableContentRef = useRef | null>(null); - // const handleLoadGap = (maxId) => { - // dispatch(expandNotifications({ maxId })); - // }; - const handleLoadOlder = useCallback( debounce( () => { - const minId = displayedNotifications.reduce( - (minId, notification) => - minId && notification.page_min_id && notification.page_min_id > minId - ? minId - : notification.page_min_id, - undefined, - ); - dispatch(expandNotifications({ maxId: minId })); + if (!hasMore || isFetchingNextPage) return; + + fetchNextPage().catch((error) => { + console.error(error); + }); }, 300, { leading: true }, ), - [displayedNotifications], + [fetchNextPage, hasMore, isFetchingNextPage], ); const handleScrollToTop = useCallback( debounce(() => { - dispatch(scrollTopNotifications(true)); + const topNotificationId = + displayedNotifications[0]?.page_max_id ?? + displayedNotifications[0]?.most_recent_notification_id; + markNotificationsRead(topNotificationId); }, 100), - [], - ); - - const handleScroll = useCallback( - debounce(() => { - dispatch(scrollTopNotifications(false)); - }, 100), - [], + [fetchNextPage, hasMore, isFetchingNextPage, displayedNotifications], ); const handleMoveUp = (id: string) => { @@ -273,27 +263,36 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => const handleDequeueNotifications = useCallback(() => { setTopNotification(undefined); - dispatch(markReadNotifications()); - }, []); - const handleRefresh = useCallback(() => dispatch(expandNotifications()), []); + markNotificationsRead(notifications[0]?.most_recent_notification_id); + }, [notifications, markNotificationsRead]); + + const handleRefresh = useCallback(() => { + queryClient.setQueryData>(['notifications', activeFilter], (data) => { + if (!data) return data; + + return { + ...data, + pages: data.pages.slice(0, 1), + pageParams: data.pageParams.slice(0, 1), + }; + }); + refetch().catch(console.error); + }, [refetch]); useEffect(() => { handleDequeueNotifications(); - dispatch(scrollTopNotifications(true)); return () => { handleLoadOlder.cancel?.(); - handleScrollToTop.cancel(); - handleScroll.cancel?.(); - dispatch(scrollTopNotifications(false)); + handleScrollToTop.cancel?.(); }; }, []); useEffect(() => { if (topNotification || displayedNotifications.length === 0) return; setTopNotification(displayedNotifications[0].most_recent_notification_id); - }, [displayedNotifications.length]); + }, [displayedNotifications, topNotification]); const emptyMessage = activeFilter === 'all' ? ( @@ -333,18 +332,15 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => {scrollableContent!} diff --git a/packages/pl-fe/src/components/helmet.tsx b/packages/pl-fe/src/components/helmet.tsx index 4728bf4fb..4acb73d1c 100644 --- a/packages/pl-fe/src/components/helmet.tsx +++ b/packages/pl-fe/src/components/helmet.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { Helmet as ReactHelmet } from 'react-helmet-async'; import { useStatContext } from '@/contexts/stat-context'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; import { usePendingUsersCount } from '@/queries/admin/use-accounts'; import { usePendingReportsCount } from '@/queries/admin/use-reports'; +import { useNotificationsUnreadCount } from '@/queries/notifications/use-notifications'; import { useSettings } from '@/stores/settings'; import FaviconService from '@/utils/favicon-service'; @@ -20,13 +20,9 @@ const Helmet: React.FC = ({ children }) => { const { unreadChatsCount } = useStatContext(); const { data: awaitingApprovalCount = 0 } = usePendingUsersCount(); const { data: pendingReportsCount = 0 } = usePendingReportsCount(); - const unreadCount = useAppSelector( - (state) => - (state.notifications.unread || 0) + - unreadChatsCount + - awaitingApprovalCount + - pendingReportsCount, - ); + const notificationCount = useNotificationsUnreadCount(); + const unreadCount = + notificationCount + unreadChatsCount + awaitingApprovalCount + pendingReportsCount; const { demetricator } = useSettings(); const hasUnreadNotifications = React.useMemo( diff --git a/packages/pl-fe/src/components/scroll-top-button.tsx b/packages/pl-fe/src/components/scroll-top-button.tsx index b8d04fd68..98b91963c 100644 --- a/packages/pl-fe/src/components/scroll-top-button.tsx +++ b/packages/pl-fe/src/components/scroll-top-button.tsx @@ -38,7 +38,7 @@ const ScrollTopButton: React.FC = ({ // Whether we are scrolled above the `autoloadThreshold`. const [scrolledTop, setScrolledTop] = useState(false); - const visible = count > 0 && (autoloadThreshold ? scrolled : scrolledTop); + const visible = count > 0 && (!autoloadTimelines || scrolled); const buttonMessage = intl.formatMessage(message, { count }); /** Number of pixels scrolled down from the top of the page. */ diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index 3722bba48..4c03f9f46 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -8,7 +8,6 @@ import Stack from '@/components/ui/stack'; import { useStatContext } from '@/contexts/stat-context'; import ComposeButton from '@/features/ui/components/compose-button'; import ProfileDropdown from '@/features/ui/components/profile-dropdown'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; import { useOwnAccount } from '@/hooks/use-own-account'; @@ -16,6 +15,7 @@ import { useRegistrationStatus } from '@/hooks/use-registration-status'; import { useFollowRequestsCount } from '@/queries/accounts/use-follow-requests'; import { usePendingUsersCount } from '@/queries/admin/use-accounts'; import { usePendingReportsCount } from '@/queries/admin/use-reports'; +import { useNotificationsUnreadCount } from '@/queries/notifications/use-notifications'; import { scheduledStatusesCountQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useDraftStatusesCountQuery } from '@/queries/statuses/use-draft-statuses'; import { useInteractionRequestsCount } from '@/queries/statuses/use-interaction-requests'; @@ -78,7 +78,7 @@ const SidebarNavigation: React.FC = React.memo(({ shrink }) [!!account, features], ); - const notificationCount = useAppSelector((state) => state.notifications.unread); + const notificationCount = useNotificationsUnreadCount(); const followRequestsCount = useFollowRequestsCount().data ?? 0; const interactionRequestsCount = useInteractionRequestsCount().data ?? 0; const { data: awaitingApprovalCount = 0 } = usePendingUsersCount(); diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index 459531122..b48b19556 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -12,6 +12,7 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useNotificationsUnreadCount } from '@/queries/notifications/use-notifications'; import { useModalsActions } from '@/stores/modals'; import { useIsSidebarOpen, useUiStoreActions } from '@/stores/ui'; import { isStandalone } from '@/utils/state'; @@ -43,7 +44,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => { const { unreadChatsCount } = useStatContext(); const standalone = useAppSelector(isStandalone); - const notificationCount = useAppSelector((state) => state.notifications.unread); + const notificationCount = useNotificationsUnreadCount(); const handleOpenComposeModal = () => { if (match?.params.groupId) { diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index 79921f852..8402d1823 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -580,4 +580,9 @@ const Notification: React.FC = (props) => { ); }; -export { Notification as default, buildLink, getNotificationStatus }; +export { + Notification as default, + buildLink, + getNotificationStatus, + messages as notificationMessages, +}; diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index dc49bd4ce..f32802024 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -4,8 +4,6 @@ import React, { Suspense, useEffect, useRef } from 'react'; import { Toaster } from 'react-hot-toast'; import { fetchConfig } from '@/actions/admin'; -import { fetchMarker } from '@/actions/markers'; -import { expandNotifications } from '@/actions/notifications'; import { register as registerPushNotifications } from '@/actions/push-notifications/registerer'; import { fetchHomeTimeline } from '@/actions/timelines'; import { useUserStream } from '@/api/hooks/streaming/use-user-stream'; @@ -22,6 +20,10 @@ import { useOwnAccount } from '@/hooks/use-own-account'; import { prefetchFollowRequests } from '@/queries/accounts/use-follow-requests'; import { queryClient } from '@/queries/client'; import { prefetchCustomEmojis } from '@/queries/instance/use-custom-emojis'; +import { + usePrefetchNotifications, + usePrefetchNotificationsMarker, +} from '@/queries/notifications/use-notifications'; import { useFilters } from '@/queries/settings/use-filters'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useSettings } from '@/stores/settings'; @@ -61,6 +63,8 @@ const UI: React.FC = React.memo(() => { useShoutboxSubscription(); useFilters(); + usePrefetchNotifications(); + usePrefetchNotificationsMarker(); const { isDragging } = useDraggedFiles(node); @@ -93,10 +97,6 @@ const UI: React.FC = React.memo(() => { dispatch(fetchHomeTimeline()); - dispatch(expandNotifications()) - .then(() => dispatch(fetchMarker(['notifications']))) - .catch(console.error); - if (account.is_admin && features.pleromaAdminAccounts) { dispatch(fetchConfig()); } diff --git a/packages/pl-fe/src/queries/notifications/use-notifications.ts b/packages/pl-fe/src/queries/notifications/use-notifications.ts new file mode 100644 index 000000000..a71b4d33b --- /dev/null +++ b/packages/pl-fe/src/queries/notifications/use-notifications.ts @@ -0,0 +1,356 @@ +import { + type InfiniteData, + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; +import 'intl-pluralrules'; +import { useCallback, useEffect } from 'react'; +import { useIntl } from 'react-intl'; + +import { importEntities } from '@/actions/importer'; +import { + getNotificationStatus, + notificationMessages, +} from '@/features/notifications/components/notification'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useAppSelector } from '@/hooks/use-app-selector'; +import { useClient } from '@/hooks/use-client'; +import { useLoggedIn } from '@/hooks/use-logged-in'; +import { normalizeNotification } from '@/normalizers/notification'; +import { appendFollowRequest } from '@/queries/accounts/use-follow-requests'; +import { queryClient } from '@/queries/client'; +import { makePaginatedResponseQueryOptions } from '@/queries/utils/make-paginated-response-query-options'; +import { getFilters, regexFromFilters } from '@/selectors'; +import { useSettingsStore } from '@/stores/settings'; +import { compareId } from '@/utils/comparators'; +import { unescapeHTML } from '@/utils/html'; +import { EXCLUDE_TYPES, NOTIFICATION_TYPES } from '@/utils/notification'; +import { play, soundCache } from '@/utils/sounds'; +import { joinPublicPath } from '@/utils/static'; + +import { minifyGroupedNotifications } from '../utils/minify-list'; + +import type { + GetGroupedNotificationsParams, + Notification, + NotificationGroup, + PaginatedResponse, +} from 'pl-api'; + +const FILTER_TYPES = { + all: undefined, + mention: ['mention', 'quote'], + favourite: ['favourite', 'emoji_reaction', 'reaction'], + reblog: ['reblog'], + poll: ['poll'], + status: ['status'], + follow: ['follow', 'follow_request'], + events: ['event_reminder', 'participation_request', 'participation_accepted'], +} as const; + +type FilterType = keyof typeof FILTER_TYPES; + +const useActiveFilter = () => + useSettingsStore((state) => state.settings.notifications.quickFilter.active); + +const excludeTypesFromFilter = (filters: string[]) => + NOTIFICATION_TYPES.filter((item) => !filters.includes(item)) as string[]; + +const buildNotificationsParams = ( + activeFilter: FilterType, + notificationsIncludeTypes: boolean, +): GetGroupedNotificationsParams => { + const params: GetGroupedNotificationsParams = {}; + + if (activeFilter === 'all') { + if (notificationsIncludeTypes) { + const excludedTypes = new Set(EXCLUDE_TYPES); + params.types = NOTIFICATION_TYPES.filter((type) => !excludedTypes.has(type)); + } else { + params.exclude_types = [...EXCLUDE_TYPES]; + } + + return params; + } + + const filtered = [...(FILTER_TYPES[activeFilter] || [activeFilter])]; + + if (notificationsIncludeTypes) { + params.types = filtered; + } else { + params.exclude_types = excludeTypesFromFilter(filtered); + } + + return params; +}; + +const shouldDisplayNotification = ( + notificationType: Notification['type'], + activeFilter: FilterType, +) => { + if (activeFilter === 'all') return true; + + const allowedTypes = [...(FILTER_TYPES[activeFilter] ?? [notificationType])] as string[]; + + return allowedTypes.includes(notificationType); +}; + +const notificationsQueryOptions = makePaginatedResponseQueryOptions( + (activeFilter: FilterType) => ['notifications', activeFilter], + (client, [activeFilter]) => + client.groupedNotifications + .getGroupedNotifications( + buildNotificationsParams(activeFilter, client.features.notificationsIncludeTypes), + ) + .then(minifyGroupedNotifications), +); + +const useNotifications = (activeFilter: FilterType) => { + const { me } = useLoggedIn(); + + return useInfiniteQuery({ + ...notificationsQueryOptions(activeFilter), + enabled: !!me, + }); +}; + +const useNotificationsMarker = () => { + const client = useClient(); + const { me } = useLoggedIn(); + + return useQuery({ + queryKey: ['markers', 'notifications'], + queryFn: async () => + (await client.timelines.getMarkers(['notifications'])).notifications ?? null, + enabled: !!me, + }); +}; + +const usePrefetchNotificationsMarker = () => { + const client = useClient(); + const queryClient = useQueryClient(); + const { me } = useLoggedIn(); + + useEffect(() => { + if (!me) return; + queryClient.prefetchQuery({ + queryKey: ['markers', 'notifications'], + queryFn: async () => + (await client.timelines.getMarkers(['notifications'])).notifications ?? null, + }); + }, [me]); +}; + +const useProcessStreamNotification = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const filters = useAppSelector((state) => getFilters(state, { contextType: 'notifications' })); + const activeFilter = useActiveFilter(); + const { sounds } = useSettingsStore((state) => state.settings.notifications); + + const processStreamNotification = useCallback( + (notification: Notification, intlMessages: Record, intlLocale: string) => { + if (!notification.type) return; + if (notification.type === 'chat_mention') return; + + const playSound = sounds[notification.type]; + const status = getNotificationStatus(notification); + + let filtered: boolean | null = false; + + if (notification.type === 'mention' || notification.type === 'status') { + const regex = regexFromFilters(filters); + const searchIndex = + notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + filtered = regex && regex.test(searchIndex); + } + + try { + const isNotificationsEnabled = window.Notification?.permission === 'granted'; + + if (!filtered && isNotificationsEnabled) { + const targetName = notification.type === 'move' ? notification.target.acct : ''; + const isReblog = status?.reblog_id ? 1 : 0; + + const title = intl.formatMessage(notificationMessages[notification.type], { + name: notification.account.display_name, + targetName, + isReblog, + }); + const body = + status && status.spoiler_text.length > 0 + ? status.spoiler_text + : unescapeHTML(status ? status.content : ''); + + navigator.serviceWorker.ready + .then((serviceWorkerRegistration) => { + serviceWorkerRegistration + .showNotification(title, { + body, + icon: notification.account.avatar, + tag: notification.id, + data: { + url: joinPublicPath('/notifications'), + }, + }) + .catch(console.error); + }) + .catch(console.error); + } + } catch (error) { + console.warn(error); + } + + if (playSound && !filtered) { + play(soundCache.boop); + } + + dispatch( + importEntities({ + accounts: [ + notification.account, + notification.type === 'move' ? notification.target : undefined, + ], + statuses: [status], + }), + ); + + const normalizedNotification = normalizeNotification(notification); + + prependNotification(normalizedNotification, 'all'); + + if (shouldDisplayNotification(notification.type, activeFilter)) { + prependNotification(normalizedNotification, activeFilter); + } + + if (normalizedNotification.type === 'follow_request') { + normalizedNotification.sample_account_ids.forEach(appendFollowRequest); + } + }, + [filters, sounds, activeFilter], + ); + + return processStreamNotification; +}; + +const useMarkNotificationsReadMutation = () => { + const client = useClient(); + + return useMutation({ + mutationKey: ['markers', 'notifications', 'save'], + mutationFn: async (lastReadId?: string | null) => { + if (!lastReadId) return; + + return await client.timelines.saveMarkers({ + notifications: { + last_read_id: lastReadId, + }, + }); + }, + onSuccess: (markers, lastReadId) => { + if (markers?.notifications) { + queryClient.setQueryData(['markers', 'notifications'], markers.notifications); + return; + } + + if (!lastReadId) return; + + queryClient.setQueryData(['markers', 'notifications'], (marker) => { + if (!marker) return undefined; + return { + ...marker, + last_read_id: lastReadId, + }; + }); + }, + }); +}; + +const countUnreadNotifications = ( + notifications: NotificationGroup[], + lastReadId?: string | null, +) => { + if (!lastReadId) return notifications.length; + + return notifications.reduce((count, notification) => { + if (compareId(notification.most_recent_notification_id, lastReadId) > 0) { + return count + 1; + } + + return count; + }, 0); +}; + +const useNotificationsUnreadCount = () => { + const { data: marker } = useNotificationsMarker(); + const { data: notifications = [] } = useNotifications('all'); + + return countUnreadNotifications(notifications, marker?.last_read_id); +}; + +const usePrefetchNotifications = () => { + const queryClient = useQueryClient(); + const { me } = useLoggedIn(); + const activeFilter = useActiveFilter(); + + useEffect(() => { + if (!me) return; + queryClient.prefetchInfiniteQuery(notificationsQueryOptions(activeFilter)); + }, [me]); +}; + +const filterUnique = ( + notification: NotificationGroup, + index: number, + notifications: Array, +) => notifications.findIndex(({ group_key }) => group_key === notification.group_key) === index; + +// For sorting the notifications +const comparator = ( + a: Pick, + b: Pick, +) => { + const length = Math.max( + a.most_recent_notification_id.length, + b.most_recent_notification_id.length, + ); + return b.most_recent_notification_id + .padStart(length, '0') + .localeCompare(a.most_recent_notification_id.padStart(length, '0')); +}; + +const prependNotification = (notification: NotificationGroup, filter: FilterType) => { + queryClient.setQueryData>>( + ['notifications', filter], + (data) => { + if (!data || !data.pages.length) return data; + + const [firstPage, ...restPages] = data.pages; + + return { + ...data, + pages: [ + { + ...firstPage, + items: [notification, ...firstPage.items].toSorted(comparator).filter(filterUnique), + }, + ...restPages, + ], + }; + }, + ); +}; + +export { + FILTER_TYPES, + type FilterType, + useMarkNotificationsReadMutation, + useNotifications, + useNotificationsMarker, + useNotificationsUnreadCount, + usePrefetchNotifications, + usePrefetchNotificationsMarker, + useProcessStreamNotification, +}; diff --git a/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts b/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts index 8730fada6..a3ca72f5a 100644 --- a/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts +++ b/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts @@ -2,37 +2,59 @@ import { type InfiniteData, infiniteQueryOptions, type QueryKey } from '@tanstac import { store } from '@/store'; -import { PaginatedResponseArray } from './make-paginated-response-query'; +import { + PaginatedResponseArray, + type PaginatedResponseQueryResult, +} from './make-paginated-response-query'; import type { PaginatedResponse, PlApiClient } from 'pl-api'; const makePaginatedResponseQueryOptions = - , T2, T3 = PaginatedResponseArray>( + < + T1 extends Array, + T2, + IsArray extends boolean = true, + T3 = PaginatedResponseQueryResult, + >( queryKey: QueryKey | ((...params: T1) => QueryKey), - queryFn: (client: PlApiClient, params: T1) => Promise>, - select?: (data: InfiniteData>) => T3, + queryFn: (client: PlApiClient, params: T1) => Promise>, + select?: (data: InfiniteData>) => T3, ) => (...params: T1) => infiniteQueryOptions({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), queryFn: ({ pageParam }) => pageParam.next?.() ?? queryFn(store.getState().auth.client, params), - initialPageParam: { previous: null, next: null, items: [], partial: false } as Awaited< - ReturnType - >, + initialPageParam: { + previous: null, + next: null, + items: [] as unknown as PaginatedResponse['items'], + partial: false, + } as Awaited>, getNextPageParam: (page) => (page.next ? page : undefined), select: select ?? ((data) => { - const items = new PaginatedResponseArray(...data.pages.map((page) => page.items).flat()); - const lastPage = data.pages.at(-1); - if (lastPage) { - items.total = lastPage.total; - items.partial = lastPage.partial; + + if (!lastPage) { + return new PaginatedResponseArray() as T3; } - return items as T3; + if (Array.isArray(lastPage.items)) { + const items = new PaginatedResponseArray( + ...data.pages.flatMap((page) => + Array.isArray(page.items) ? page.items : [page.items], + ), + ); + + items.total = lastPage.total; + items.partial = lastPage.partial; + + return items as T3; + } + + return lastPage.items as T3; }), }); diff --git a/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts b/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts index ea7703b11..4bc1c3306 100644 --- a/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts +++ b/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts @@ -10,11 +10,22 @@ class PaginatedResponseArray extends Array { partial?: boolean; } +type PaginatedResponseQueryResult = IsArray extends true + ? PaginatedResponseArray + : T extends Array + ? PaginatedResponseArray + : T; + const makePaginatedResponseQuery = - , T2, T3 = PaginatedResponseArray>( + < + T1 extends Array, + T2, + IsArray extends boolean = true, + T3 = PaginatedResponseQueryResult, + >( queryKey: QueryKey | ((...params: T1) => QueryKey), - queryFn: (client: PlApiClient, params: T1) => Promise>, - select?: (data: InfiniteData>) => T3, + queryFn: (client: PlApiClient, params: T1) => Promise>, + select?: (data: InfiniteData>) => T3, enabled?: ((...params: T1) => boolean) | 'isLoggedIn' | 'isAdmin', ) => (...params: T1) => { @@ -24,22 +35,36 @@ const makePaginatedResponseQuery = return useInfiniteQuery({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), queryFn: ({ pageParam }) => pageParam.next?.() ?? queryFn(client, params), - initialPageParam: { previous: null, next: null, items: [], partial: false } as Awaited< - ReturnType - >, + initialPageParam: { + previous: null, + next: null, + items: [] as unknown as PaginatedResponse['items'], + partial: false, + } as Awaited>, getNextPageParam: (page) => (page.next ? page : undefined), select: select ?? ((data) => { - const items = new PaginatedResponseArray(...data.pages.map((page) => page.items).flat()); - const lastPage = data.pages.at(-1); - if (lastPage) { - items.total = lastPage.total; - items.partial = lastPage.partial; + + if (!lastPage) { + return new PaginatedResponseArray() as T3; } - return items as T3; + if (Array.isArray(lastPage.items)) { + const items = new PaginatedResponseArray( + ...data.pages.flatMap((page) => + Array.isArray(page.items) ? page.items : [page.items], + ), + ); + + items.total = lastPage.total; + items.partial = lastPage.partial; + + return items as T3; + } + + return lastPage.items as T3; }), enabled: enabled === 'isLoggedIn' @@ -50,4 +75,4 @@ const makePaginatedResponseQuery = }); }; -export { makePaginatedResponseQuery, PaginatedResponseArray }; +export { makePaginatedResponseQuery, PaginatedResponseArray, type PaginatedResponseQueryResult }; diff --git a/packages/pl-fe/src/queries/utils/minify-list.ts b/packages/pl-fe/src/queries/utils/minify-list.ts index 3d66a8934..976a2b895 100644 --- a/packages/pl-fe/src/queries/utils/minify-list.ts +++ b/packages/pl-fe/src/queries/utils/minify-list.ts @@ -10,25 +10,35 @@ import type { BlockedAccount, Conversation, Group, + GroupedNotificationsResults, MutedAccount, + NotificationGroup, PaginatedResponse, Status, } from 'pl-api'; -const minifyList = ( - { previous, next, items, ...response }: PaginatedResponse, +const minifyList = ( + { previous, next, items, ...response }: PaginatedResponse, minifier: (value: T1) => T2, - importer?: (items: Array) => void, -): PaginatedResponse => { + importer?: (items: PaginatedResponse['items']) => void, + isArray: IsArray = true as IsArray, +): PaginatedResponse => { importer?.(items); + const minifiedItems = ( + isArray ? (items as T1[]).map(minifier) : minifier(items as T1) + ) as PaginatedResponse['items']; + return { ...response, previous: previous - ? () => previous().then((list) => minifyList(list, minifier, importer)) + ? () => + previous().then((list) => minifyList(list, minifier, importer, isArray)) : null, - next: next ? () => next().then((list) => minifyList(list, minifier, importer)) : null, - items: items.map(minifier), + next: next + ? () => next().then((list) => minifyList(list, minifier, importer, isArray)) + : null, + items: minifiedItems, }; }; @@ -103,6 +113,25 @@ const minifyConversationList = (response: PaginatedResponse) => ); }); +const minifyGroupedNotifications = ( + response: PaginatedResponse, +): PaginatedResponse => + minifyList( + response, + (results) => results.notification_groups, + (results) => { + const { accounts, statuses } = results; + + store.dispatch( + importEntities({ + accounts, + statuses, + }) as any, + ); + }, + false, + ); + const minifyAdminAccount = ({ account, ...adminAccount }: AdminAccount) => { store.dispatch(importEntities({ accounts: [account] }) as any); queryClient.setQueryData(['admin', 'accounts', adminAccount.id], adminAccount); @@ -182,6 +211,7 @@ export { minifyGroupList, minifyConversation, minifyConversationList, + minifyGroupedNotifications, minifyAdminAccount, minifyAdminAccountList, minifyAdminReport, diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 9dd303b14..91aa36555 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -12,7 +12,6 @@ import frontendConfig from './frontend-config'; import instance from './instance'; import me from './me'; import meta from './meta'; -import notifications from './notifications'; import push_notifications from './push-notifications'; import statuses from './statuses'; import timelines from './timelines'; @@ -27,7 +26,6 @@ const reducers = { instance, me, meta, - notifications, push_notifications, statuses, timelines, diff --git a/packages/pl-fe/src/reducers/notifications.ts b/packages/pl-fe/src/reducers/notifications.ts deleted file mode 100644 index 85a8a17ce..000000000 --- a/packages/pl-fe/src/reducers/notifications.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { create } from 'mutative'; - -import { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - type AccountsAction, -} from '../actions/accounts'; -import { MARKER_FETCH_SUCCESS, MARKER_SAVE_SUCCESS, type MarkersAction } from '../actions/markers'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_EXPAND_SUCCESS, - NOTIFICATIONS_EXPAND_REQUEST, - NOTIFICATIONS_EXPAND_FAIL, - NOTIFICATIONS_FILTER_SET, - NOTIFICATIONS_SCROLL_TOP, - type NotificationsAction, -} from '../actions/notifications'; -import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; - -import type { - GroupedNotificationsResults, - Markers, - NotificationGroup, - PaginatedResponse, - Relationship, -} from 'pl-api'; - -interface State { - items: Array; - hasMore: boolean; - top: boolean; - unread: number; - isLoading: boolean; - lastRead: string | -1; -} - -const initialState: State = { - items: [], - hasMore: true, - top: false, - unread: 0, - isLoading: false, - lastRead: -1, -}; - -const filterUnique = ( - notification: NotificationGroup, - index: number, - notifications: Array, -) => notifications.findIndex(({ group_key }) => group_key === notification.group_key) === index; - -// For sorting the notifications -const comparator = ( - a: Pick, - b: Pick, -) => { - const length = Math.max( - a.most_recent_notification_id.length, - b.most_recent_notification_id.length, - ); - return b.most_recent_notification_id - .padStart(length, '0') - .localeCompare(a.most_recent_notification_id.padStart(length, '0')); -}; - -// Count how many notifications appear after the given ID (for unread count) -const countFuture = (notifications: Array, lastId: string | number) => - notifications.reduce((acc, notification) => { - const length = Math.max( - notification.most_recent_notification_id.length, - lastId.toString().length, - ); - if ( - notification.most_recent_notification_id - .padStart(length, '0') - .localeCompare(lastId.toString().padStart(length, '0')) === 1 - ) { - return acc + 1; - } else { - return acc; - } - }, 0); - -const importNotification = (state: State, notification: NotificationGroup) => - create(state, (draft) => { - const top = draft.top; - if (!top) draft.unread += 1; - - draft.items = [notification, ...draft.items].toSorted(comparator).filter(filterUnique); - }); - -const expandNormalizedNotifications = ( - state: State, - notifications: NotificationGroup[], - next: (() => Promise>) | null, -) => - create(state, (draft) => { - draft.items = [...notifications, ...draft.items].toSorted(comparator).filter(filterUnique); - - if (!next) draft.hasMore = false; - draft.isLoading = false; - }); - -const filterNotifications = (state: State, relationship: Relationship) => - create(state, (draft) => { - draft.items = draft.items.filter((item) => !item.sample_account_ids.includes(relationship.id)); - }); - -// const filterNotificationIds = (state: State, accountIds: Array, type?: string) => -// create(state, (draft) => { -// const helper = (list: Array) => list.filter(item => !(accountIds.includes(item.sample_account_ids[0]) && (type === undefined || type === item.type))); -// draft.items = helper(draft.items); -// }); - -const updateTop = (state: State, top: boolean) => - create(state, (draft) => { - if (top) draft.unread = 0; - draft.top = top; - }); - -const deleteByStatus = (state: State, statusId: string) => - create(state, (draft) => { - // @ts-ignore - draft.items = draft.items.filterNot((item) => item !== null && item.status_id === statusId); - }); - -const importMarker = (state: State, marker: Markers) => { - const lastReadId = marker.notifications?.last_read_id || (-1 as string | -1); - - if (!lastReadId) { - return state; - } - - return create(state, (draft) => { - const notifications = draft.items; - const unread = countFuture(notifications, lastReadId); - - draft.unread = unread; - draft.lastRead = lastReadId; - }); -}; - -const notifications = ( - state: State = initialState, - action: AccountsAction | MarkersAction | NotificationsAction | TimelineAction, -): State => { - switch (action.type) { - case NOTIFICATIONS_EXPAND_REQUEST: - return create(state, (draft) => { - draft.isLoading = true; - }); - case NOTIFICATIONS_EXPAND_FAIL: - if ((action.error as any)?.message === 'canceled') return state; - return create(state, (draft) => { - draft.isLoading = false; - }); - case NOTIFICATIONS_FILTER_SET: - return create(state, (draft) => { - draft.items = []; - draft.hasMore = true; - }); - case NOTIFICATIONS_SCROLL_TOP: - return updateTop(state, action.top); - case NOTIFICATIONS_UPDATE: - return importNotification(state, action.notification); - case NOTIFICATIONS_EXPAND_SUCCESS: - return expandNormalizedNotifications(state, action.notifications, action.next); - case ACCOUNT_BLOCK_SUCCESS: - return filterNotifications(state, action.relationship); - case ACCOUNT_MUTE_SUCCESS: - return action.relationship.muting_notifications - ? filterNotifications(state, action.relationship) - : state; - // case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: - // case FOLLOW_REQUEST_REJECT_SUCCESS: - // return filterNotificationIds(state, [action.accountId], 'follow_request'); - case MARKER_FETCH_SUCCESS: - case MARKER_SAVE_SUCCESS: - return importMarker(state, action.marker); - case TIMELINE_DELETE: - return deleteByStatus(state, action.statusId); - default: - return state; - } -}; - -export { notifications as default }; diff --git a/packages/pl-fe/src/schemas/pl-fe/settings.ts b/packages/pl-fe/src/schemas/pl-fe/settings.ts index 2e7c8122d..b5b029ee9 100644 --- a/packages/pl-fe/src/schemas/pl-fe/settings.ts +++ b/packages/pl-fe/src/schemas/pl-fe/settings.ts @@ -107,7 +107,10 @@ const settingsSchema = v.object({ notifications: coerceObject({ quickFilter: coerceObject({ - active: v.optional(v.string(), 'all'), + active: v.optional( + v.picklist(['all', 'mention', 'favourite', 'reblog', 'poll', 'status', 'follow', 'events']), + 'all', + ), advanced: v.optional(v.boolean(), false), show: v.optional(v.boolean(), true), }), From 15444f00e16a6259c3432b6b4a0725d6ba6c12c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Mon, 23 Feb 2026 22:56:15 +0100 Subject: [PATCH 038/264] nicolium: i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/locales/en.json | 15 +++++++++------ packages/pl-fe/src/pages/settings/edit-filter.tsx | 15 +++++++++++++-- packages/pl-fe/src/pages/settings/filters.tsx | 9 ++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 2898ad51e..b09876fea 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -488,12 +488,16 @@ "column.filters.accounts": "Accounts", "column.filters.add_new": "Add new filter", "column.filters.conversations": "Conversations", - "column.filters.create_error": "Error adding filter", + "column.filters.create.error": "Error adding filter", + "column.filters.create.success": "Filter added successfully", "column.filters.delete": "Delete", - "column.filters.delete_error": "Error deleting filter", + "column.filters.delete.error": "Error deleting filter", + "column.filters.delete.success": "Filter deleted successfully", "column.filters.drop_header": "Drop instead of hide", "column.filters.drop_hint": "Filtered posts will disappear irreversibly, even if filter is later removed", "column.filters.edit": "Edit filter", + "column.filters.edit.error": "Error editing filter", + "column.filters.edit.success": "Filter edited successfully", "column.filters.expiration.1800": "30 minutes", "column.filters.expiration.21600": "6 hours", "column.filters.expiration.3600": "1 hour", @@ -1052,7 +1056,6 @@ "fediverse_tab.explanation_box.explanation": "{site_title} is part of the Fediverse, a social network made up of thousands of independent social media sites (aka \"servers\"). Here, you can see public posts from across the Fediverse, including other servers. Pay attention to the full username after the second @ symbol to know which server a post is from. To see only {site_title} posts, visit {local}.", "fediverse_tab.explanation_box.title": "What is the Fediverse?", "feed_suggestions.view_all": "View all", - "filters.added": "Filter added.", "filters.context_header": "Filter contexts", "filters.context_hint": "One or multiple contexts where the filter should apply", "filters.create_filter": "Create filter", @@ -1064,8 +1067,6 @@ "filters.filters_list_hide_completely": "Hide content", "filters.filters_list_phrases_label": "Keywords or phrases:", "filters.filters_list_warn": "Display warning", - "filters.removed": "Filter deleted.", - "filters.updated": "Filter updated.", "follow_request.authorize": "Authorize", "follow_request.reject": "Reject", "footer.meow": "meow :3 {emoji}", @@ -1270,7 +1271,7 @@ "lightbox.view_context": "View context", "lightbox.zoom_in": "Zoom to actual size", "lightbox.zoom_out": "Zoom to fit", - "link_preview.more_from_author": "More from {name}", + "link_preview.more_from_author": "From {name}", "list.click_to_add": "Click here to add people", "list_adder.header_title": "Add or remove from lists", "lists.account.add": "Add to list", @@ -1753,7 +1754,9 @@ "select_bookmark_folder_modal.header_title": "Select folder", "settings.configure_mfa": "Configure MFA", "settings.edit_profile": "Edit profile", + "settings.messages.fail": "Failed to update chat settings", "settings.messages.label": "Allow users to start a new chat with you", + "settings.messages.success": "Chat settings updated successfully", "settings.mutes_blocks": "Mutes and blocks", "settings.other": "Other options", "settings.privacy": "Privacy", diff --git a/packages/pl-fe/src/pages/settings/edit-filter.tsx b/packages/pl-fe/src/pages/settings/edit-filter.tsx index e654f7c95..fe2742904 100644 --- a/packages/pl-fe/src/pages/settings/edit-filter.tsx +++ b/packages/pl-fe/src/pages/settings/edit-filter.tsx @@ -75,7 +75,13 @@ const messages = defineMessages({ }, add_new: { id: 'column.filters.add_new', defaultMessage: 'Add new filter' }, edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' }, - createError: { id: 'column.filters.create_error', defaultMessage: 'Error adding filter' }, + createError: { id: 'column.filters.create.error', defaultMessage: 'Error adding filter' }, + editError: { id: 'column.filters.edit.error', defaultMessage: 'Error editing filter' }, + createSuccess: { + id: 'column.filters.create.success', + defaultMessage: 'Filter added successfully', + }, + editSuccess: { id: 'column.filters.edit.success', defaultMessage: 'Filter edited successfully' }, expiration_never: { id: 'column.filters.expiration.never', defaultMessage: 'Never' }, expiration_1800: { id: 'column.filters.expiration.1800', defaultMessage: '30 minutes' }, expiration_3600: { id: 'column.filters.expiration.3600', defaultMessage: '1 hour' }, @@ -190,9 +196,14 @@ const EditFilterPage: React.FC = () => { { onSuccess: () => { navigate({ to: '/filters' }); + toast.success( + intl.formatMessage(filterId !== 'new' ? messages.editSuccess : messages.createSuccess), + ); }, onError: () => { - toast.error(intl.formatMessage(messages.createError)); + toast.error( + intl.formatMessage(filterId !== 'new' ? messages.editError : messages.createError), + ); }, }, ); diff --git a/packages/pl-fe/src/pages/settings/filters.tsx b/packages/pl-fe/src/pages/settings/filters.tsx index 5fe517f5e..5be2babfd 100644 --- a/packages/pl-fe/src/pages/settings/filters.tsx +++ b/packages/pl-fe/src/pages/settings/filters.tsx @@ -19,7 +19,11 @@ const messages = defineMessages({ notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' }, conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' }, accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' }, - deleteError: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' }, + deleteSuccess: { + id: 'column.filters.delete.success', + defaultMessage: 'Filter deleted successfully', + }, + deleteError: { id: 'column.filters.delete.error', defaultMessage: 'Error deleting filter' }, edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' }, delete: { id: 'column.filters.delete', defaultMessage: 'Delete' }, }); @@ -41,6 +45,9 @@ const FiltersPage = () => { const handleFilterDelete = (id: string) => () => { deleteFilter(id, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.deleteSuccess)); + }, onError: () => { toast.error(intl.formatMessage(messages.deleteError)); }, From f847e93293d6bf4dc625441114b611d0c4322718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 11:25:41 +0100 Subject: [PATCH 039/264] nicolium: i18n cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../compose/components/privacy-dropdown.tsx | 88 ++++---- .../pl-fe/src/pages/settings/edit-filter.tsx | 189 ++++++++++-------- 2 files changed, 154 insertions(+), 123 deletions(-) diff --git a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx index 1dac277fb..1545d118c 100644 --- a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx +++ b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx @@ -14,37 +14,37 @@ import { useLists } from '@/queries/accounts/use-lists'; import type { Circle, Features } from 'pl-api'; const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Not visible in public timelines' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, - conversation_short: { id: 'privacy.conversation.short', defaultMessage: 'Conversation' }, - conversation_long: { + publicShort: { id: 'privacy.public.short', defaultMessage: 'Public' }, + publicLong: { id: 'privacy.public.long', defaultMessage: 'Post to public timelines' }, + unlistedShort: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, + unlistedLong: { id: 'privacy.unlisted.long', defaultMessage: 'Not visible in public timelines' }, + privateShort: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, + privateLong: { id: 'privacy.private.long', defaultMessage: 'Post to followers only' }, + conversationShort: { id: 'privacy.conversation.short', defaultMessage: 'Conversation' }, + conversationLong: { id: 'privacy.conversation.long', defaultMessage: 'Post to recipients of the parent post', }, - mutuals_only_short: { id: 'privacy.mutuals_only.short', defaultMessage: 'Mutuals-only' }, - mutuals_only_long: { + mutualsOnlyShort: { id: 'privacy.mutuals_only.short', defaultMessage: 'Mutuals-only' }, + mutualsOnlyLong: { id: 'privacy.mutuals_only.long', defaultMessage: 'Post to mutually followed users only', }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Private mention' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible to mentioned users only' }, - local_short: { id: 'privacy.local.short', defaultMessage: 'Local-only' }, - local_long: { id: 'privacy.local.long', defaultMessage: 'Only visible on your instance' }, - list_short: { id: 'privacy.list.short', defaultMessage: 'List only' }, - list_long: { id: 'privacy.list.long', defaultMessage: 'Visible to members of a list' }, - circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle only' }, - circle_long: { id: 'privacy.circle.long', defaultMessage: 'Visible to members of a circle' }, - subscribers_short: { id: 'privacy.subscribers.short', defaultMessage: 'Subscribers-only' }, - subscribers_long: { + directShort: { id: 'privacy.direct.short', defaultMessage: 'Private mention' }, + directLong: { id: 'privacy.direct.long', defaultMessage: 'Visible to mentioned users only' }, + localShort: { id: 'privacy.local.short', defaultMessage: 'Local-only' }, + localLong: { id: 'privacy.local.long', defaultMessage: 'Only visible on your instance' }, + listShort: { id: 'privacy.list.short', defaultMessage: 'List only' }, + listLong: { id: 'privacy.list.long', defaultMessage: 'Visible to members of a list' }, + circleShort: { id: 'privacy.circle.short', defaultMessage: 'Circle only' }, + circleLong: { id: 'privacy.circle.long', defaultMessage: 'Visible to members of a circle' }, + subscribersShort: { id: 'privacy.subscribers.short', defaultMessage: 'Subscribers-only' }, + subscribersLong: { id: 'privacy.subscribers.long', defaultMessage: 'Post to users subscribing you only', }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' }, + changePrivacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' }, local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' }, }); @@ -67,57 +67,57 @@ const getItems = ( { icon: require('@phosphor-icons/core/regular/globe.svg'), value: 'public', - text: intl.formatMessage(messages.public_short), - meta: intl.formatMessage(messages.public_long), + text: intl.formatMessage(messages.publicShort), + meta: intl.formatMessage(messages.publicLong), }, { icon: require('@phosphor-icons/core/regular/moon.svg'), value: 'unlisted', - text: intl.formatMessage(messages.unlisted_short), - meta: intl.formatMessage(messages.unlisted_long), + text: intl.formatMessage(messages.unlistedShort), + meta: intl.formatMessage(messages.unlistedLong), }, { icon: require('@phosphor-icons/core/regular/lock.svg'), value: 'private', - text: intl.formatMessage(messages.private_short), - meta: intl.formatMessage(messages.private_long), + text: intl.formatMessage(messages.privateShort), + meta: intl.formatMessage(messages.privateLong), }, isReply && features.createStatusConversationScope ? { icon: require('@phosphor-icons/core/regular/chats-circle.svg'), value: 'conversation', - text: intl.formatMessage(messages.conversation_short), - meta: intl.formatMessage(messages.conversation_long), + text: intl.formatMessage(messages.conversationShort), + meta: intl.formatMessage(messages.conversationLong), } : undefined, features.createStatusMutualsOnlyScope ? { icon: require('@phosphor-icons/core/regular/users-three.svg'), value: 'mutuals_only', - text: intl.formatMessage(messages.mutuals_only_short), - meta: intl.formatMessage(messages.mutuals_only_long), + text: intl.formatMessage(messages.mutualsOnlyShort), + meta: intl.formatMessage(messages.mutualsOnlyLong), } : undefined, features.createStatusSubscribersScope ? { icon: require('@phosphor-icons/core/regular/coins.svg'), value: 'subscribers', - text: intl.formatMessage(messages.subscribers_short), - meta: intl.formatMessage(messages.subscribers_long), + text: intl.formatMessage(messages.subscribersShort), + meta: intl.formatMessage(messages.subscribersLong), } : undefined, { icon: require('@phosphor-icons/core/regular/at.svg'), value: 'direct', - text: intl.formatMessage(messages.direct_short), - meta: intl.formatMessage(messages.direct_long), + text: intl.formatMessage(messages.directShort), + meta: intl.formatMessage(messages.directLong), }, features.createStatusLocalScope ? { icon: require('@phosphor-icons/core/regular/planet.svg'), value: 'local', - text: intl.formatMessage(messages.local_short), - meta: intl.formatMessage(messages.local_long), + text: intl.formatMessage(messages.localShort), + meta: intl.formatMessage(messages.localLong), } : undefined, features.createStatusListScope && Object.keys(lists).length @@ -129,8 +129,8 @@ const getItems = ( value: `list:${list.id}`, text: list.title, })), - text: intl.formatMessage(messages.list_short), - meta: intl.formatMessage(messages.list_long), + text: intl.formatMessage(messages.listShort), + meta: intl.formatMessage(messages.listLong), } as Option) : undefined, features.circles && Object.keys(circles).length @@ -142,8 +142,8 @@ const getItems = ( value: `circle:${circle.id}`, text: circle.title, })), - text: intl.formatMessage(messages.circle_short), - meta: intl.formatMessage(messages.circle_long), + text: intl.formatMessage(messages.circleShort), + meta: intl.formatMessage(messages.circleLong), } as Option) : undefined, ].filter((option): option is Option => !!option); @@ -187,8 +187,8 @@ const PrivacyDropdown: React.FC = ({ composeId, compact }) => if (features.localOnlyStatuses) items.push({ icon: require('@phosphor-icons/core/regular/planet.svg'), - text: intl.formatMessage(messages.local_short), - meta: intl.formatMessage(messages.local_long), + text: intl.formatMessage(messages.localShort), + meta: intl.formatMessage(messages.localLong), type: 'toggle', checked: compose.localOnly, onChange: () => dispatch(changeComposeFederated(composeId)), @@ -220,7 +220,7 @@ const PrivacyDropdown: React.FC = ({ composeId, compact }) => return ( - From 5f95d3d7a2622370a94312f134685b76875e117c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 12:30:21 +0100 Subject: [PATCH 040/264] nicolium: use camelcase for message definitions, prefer s MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/account.tsx | 6 +- packages/pl-fe/src/components/load-gap.tsx | 4 +- .../src/components/relative-timestamp.tsx | 24 ++-- .../src/components/status-action-bar.tsx | 32 ++--- packages/pl-fe/src/components/status.tsx | 4 +- packages/pl-fe/src/components/ui/avatar.tsx | 8 +- .../features/account/components/header.tsx | 21 +--- .../components/registration-form.tsx | 63 +++++----- .../components/content-type-button.tsx | 24 ++-- .../compose/components/location-button.tsx | 8 +- .../compose/components/poll-button.tsx | 6 +- .../compose/components/polls/poll-form.tsx | 7 +- .../compose/components/schedule-button.tsx | 6 +- .../components/emoji-picker-dropdown.tsx | 52 ++++---- .../event/components/event-header.tsx | 37 +++--- .../pl-fe/src/features/preferences/index.tsx | 28 ++--- .../security/mfa/disable-otp-form.tsx | 5 +- .../features/ui/components/action-button.tsx | 110 +++++++++++------ .../components/panels/profile-info-panel.tsx | 4 +- .../ui/components/panels/user-panel.tsx | 4 +- packages/pl-fe/src/features/video/index.tsx | 8 +- packages/pl-fe/src/locales/en.json | 15 +-- packages/pl-fe/src/modals/boost-modal.tsx | 14 +-- packages/pl-fe/src/pages/settings/aliases.tsx | 25 ++-- .../pl-fe/src/pages/settings/export-data.tsx | 91 +++++++------- .../pl-fe/src/pages/settings/import-data.tsx | 111 ++++++++++-------- .../pages/settings/interaction-policies.tsx | 26 ++-- 27 files changed, 392 insertions(+), 351 deletions(-) diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 76766aada..3de4f3f09 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -33,7 +33,7 @@ interface IInstanceFavicon { const messages = defineMessages({ bot: { id: 'account.badges.bot', defaultMessage: 'Bot' }, timeline: { id: 'account.instance_favicon', defaultMessage: 'Visit {domain} timeline' }, - account_locked: { + accountLocked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.', @@ -302,7 +302,7 @@ const Account = ({ <> @@ -413,7 +413,7 @@ const Account = ({ <> {account.favicon && !disableUserProvidedMedia && ( diff --git a/packages/pl-fe/src/components/load-gap.tsx b/packages/pl-fe/src/components/load-gap.tsx index 5bfacf7e6..b604eba85 100644 --- a/packages/pl-fe/src/components/load-gap.tsx +++ b/packages/pl-fe/src/components/load-gap.tsx @@ -4,7 +4,7 @@ import { defineMessages, useIntl } from 'react-intl'; import Icon from '@/components/icon'; const messages = defineMessages({ - load_more: { id: 'status.load_more', defaultMessage: 'Load more' }, + loadMore: { id: 'status.load_more', defaultMessage: 'Load more' }, }); interface ILoadGap { @@ -25,7 +25,7 @@ const LoadGap: React.FC = ({ disabled, maxId, onClick }) => { className='m-0 box-border block w-full border-0 bg-transparent p-4 text-gray-900' disabled={disabled} onClick={handleClick} - aria-label={intl.formatMessage(messages.load_more)} + aria-label={intl.formatMessage(messages.loadMore)} > diff --git a/packages/pl-fe/src/components/relative-timestamp.tsx b/packages/pl-fe/src/components/relative-timestamp.tsx index 9b0004a2a..28689cdf3 100644 --- a/packages/pl-fe/src/components/relative-timestamp.tsx +++ b/packages/pl-fe/src/components/relative-timestamp.tsx @@ -4,25 +4,25 @@ import { injectIntl, defineMessages, IntlShape, FormatDateOptions } from 'react- import Text, { IText } from './ui/text'; const messages = defineMessages({ - just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, + justNow: { id: 'relative_time.just_now', defaultMessage: 'now' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, hours: { id: 'relative_time.hours', defaultMessage: '{number}h' }, days: { id: 'relative_time.days', defaultMessage: '{number}d' }, - moments_remaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, - seconds_remaining: { + momentsRemaining: { id: 'time_remaining.moments', defaultMessage: 'Moments remaining' }, + secondsRemaining: { id: 'time_remaining.seconds', defaultMessage: '{number, plural, one {# second} other {# seconds}} left', }, - minutes_remaining: { + minutesRemaining: { id: 'time_remaining.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}} left', }, - hours_remaining: { + hoursRemaining: { id: 'time_remaining.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}} left', }, - days_remaining: { + daysRemaining: { id: 'time_remaining.days', defaultMessage: '{number, plural, one {# day} other {# days}} left', }, @@ -84,7 +84,7 @@ const timeAgoString = (intl: IntlShape, date: Date, now: number, year: number) = let relativeTime; if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.just_now); + relativeTime = intl.formatMessage(messages.justNow); } else if (delta < 7 * DAY) { if (delta < MINUTE) { relativeTime = intl.formatMessage(messages.seconds, { number: Math.floor(delta / SECOND) }); @@ -110,21 +110,21 @@ const timeRemainingString = (intl: IntlShape, date: Date, now: number) => { let relativeTime; if (delta < 10 * SECOND) { - relativeTime = intl.formatMessage(messages.moments_remaining); + relativeTime = intl.formatMessage(messages.momentsRemaining); } else if (delta < MINUTE) { - relativeTime = intl.formatMessage(messages.seconds_remaining, { + relativeTime = intl.formatMessage(messages.secondsRemaining, { number: Math.floor(delta / SECOND), }); } else if (delta < HOUR) { - relativeTime = intl.formatMessage(messages.minutes_remaining, { + relativeTime = intl.formatMessage(messages.minutesRemaining, { number: Math.floor(delta / MINUTE), }); } else if (delta < DAY) { - relativeTime = intl.formatMessage(messages.hours_remaining, { + relativeTime = intl.formatMessage(messages.hoursRemaining, { number: Math.floor(delta / HOUR), }); } else { - relativeTime = intl.formatMessage(messages.days_remaining, { number: Math.floor(delta / DAY) }); + relativeTime = intl.formatMessage(messages.daysRemaining, { number: Math.floor(delta / DAY) }); } return relativeTime; diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index b753bf519..e6c59bb02 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -57,7 +57,7 @@ import type { Me } from '@/types/pl-fe'; const messages = defineMessages({ adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, - admin_status: { + adminStatus: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface', }, @@ -70,8 +70,8 @@ const messages = defineMessages({ 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' }, + cancelReblogPrivate: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, + cannotReblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be reposted' }, chat: { id: 'status.chat', defaultMessage: 'Chat with @{name}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to post' }, deactivateUser: { @@ -107,14 +107,6 @@ const messages = defineMessages({ defaultMessage: 'Are you sure you want to ban @{name} from the group?', }, groupModDelete: { id: 'status.group_mod_delete', defaultMessage: 'Delete post from group' }, - group_remove_account: { - id: 'status.remove_account_from_group', - defaultMessage: 'Remove account from group', - }, - group_remove_post: { - id: 'status.remove_post_from_group', - defaultMessage: 'Remove post from group', - }, loadConversation: { id: 'status.load_conversation', defaultMessage: 'Load conversation from remote server', @@ -351,7 +343,7 @@ const ReplyButton: React.FC = ({ if (group?.membership_required && !group.relationship?.member) { replyDisabled = true; - replyTitle = intl.formatMessage(messages.replies_disabled_group); + replyTitle = intl.formatMessage(messages.repliesDisabledGroup); } if (!status.in_reply_to_id) { @@ -469,7 +461,7 @@ const ReblogButton: React.FC = ({ disabled={!publicStatus} title={ !publicStatus - ? intl.formatMessage(messages.cannot_reblog) + ? intl.formatMessage(messages.cannotReblog) : intl.formatMessage(messages.reblog) } active={status.reblogged} @@ -502,7 +494,7 @@ const ReblogButton: React.FC = ({ const reblogMenu = [ { - text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), + text: intl.formatMessage(status.reblogged ? messages.cancelReblogPrivate : messages.reblog), action: handleReblogClick, icon: require('@phosphor-icons/core/regular/repeat.svg'), }, @@ -1087,25 +1079,25 @@ const MenuButton: React.FC = ({ if (publicStatus && !status.reblogged && features.reblogVisibility) { menu.push({ - text: intl.formatMessage(messages.reblog_visibility), + text: intl.formatMessage(messages.reblogVisibility), icon: require('@phosphor-icons/core/regular/repeat.svg'), items: [ { - text: intl.formatMessage(messages.reblog_visibility_public), + text: intl.formatMessage(messages.reblogVisibilityPublic), action: (e) => { handleReblogClick(e, 'public'); }, icon: require('@phosphor-icons/core/regular/globe.svg'), }, { - text: intl.formatMessage(messages.reblog_visibility_unlisted), + text: intl.formatMessage(messages.reblogVisibilityUnlisted), action: (e) => { handleReblogClick(e, 'unlisted'); }, icon: require('@phosphor-icons/core/regular/moon.svg'), }, { - text: intl.formatMessage(messages.reblog_visibility_private), + text: intl.formatMessage(messages.reblogVisibilityPrivate), action: (e) => { handleReblogClick(e, 'private'); }, @@ -1127,7 +1119,7 @@ const MenuButton: React.FC = ({ } else if (status.visibility === 'private' || status.visibility === 'mutuals_only') { menu.push({ text: intl.formatMessage( - status.reblogged ? messages.cancel_reblog_private : messages.reblog_private, + status.reblogged ? messages.cancelReblogPrivate : messages.reblogPrivate, ), action: handleReblogClick, icon: require('@phosphor-icons/core/regular/repeat.svg'), @@ -1269,7 +1261,7 @@ const MenuButton: React.FC = ({ if (isAdmin && features.pleromaAdminStatuses) { menu.push({ - text: intl.formatMessage(messages.admin_status), + text: intl.formatMessage(messages.adminStatus), href: `/pleroma/admin/#/statuses/${status.id}/`, icon: require('@phosphor-icons/core/regular/pencil-simple.svg'), }); diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index ce1a34b1a..0cef982c2 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -42,7 +42,7 @@ import Tombstone from './tombstone'; const messages = defineMessages({ edited: { id: 'status.edited', defaultMessage: 'Edited {date}' }, - reblogged_by: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, + rebloggedBy: { id: 'status.reblogged_by', defaultMessage: '{name} reposted' }, }); interface IAccountInfo { @@ -554,7 +554,7 @@ const Status: React.FC = (props) => { let rebloggedByText; if (status.reblog_id === 'object') { - rebloggedByText = intl.formatMessage(messages.reblogged_by, { name: status.account.acct }); + rebloggedByText = intl.formatMessage(messages.rebloggedBy, { name: status.account.acct }); } const body = ( diff --git a/packages/pl-fe/src/components/ui/avatar.tsx b/packages/pl-fe/src/components/ui/avatar.tsx index ebcdbafb1..ccb9a4173 100644 --- a/packages/pl-fe/src/components/ui/avatar.tsx +++ b/packages/pl-fe/src/components/ui/avatar.tsx @@ -19,11 +19,11 @@ const AVATAR_SIZE = 42; const messages = defineMessages({ avatar: { id: 'account.avatar.alt', defaultMessage: 'Avatar' }, - avatar_with_username: { + avatarWithUsername: { id: 'account.avatar.with_username', defaultMessage: 'Avatar for {username}', }, - avatar_with_content: { + avatarWithContent: { id: 'account.avatar.with_content', defaultMessage: 'Avatar for {username}: {alt}', }, @@ -134,9 +134,9 @@ const Avatar = (props: IAvatar) => { const altText = props.showAlt && alt - ? intl.formatMessage(messages.avatar_with_content, { username: props.username, alt }) + ? intl.formatMessage(messages.avatarWithContent, { username: props.username, alt }) : props.username - ? intl.formatMessage(messages.avatar_with_username, { username: props.username }) + ? intl.formatMessage(messages.avatarWithUsername, { username: props.username }) : intl.formatMessage(messages.avatar); return ( diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index 16db231de..a69aca6d8 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -52,16 +52,7 @@ import type { PlfeResponse } from '@/api'; import type { Account as AccountEntity } from 'pl-api'; const messages = defineMessages({ - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - linkVerifiedOn: { - id: 'account.link_verified_on', - defaultMessage: 'Ownership of this link was checked on {date}', - }, - account_locked: { - id: 'account.locked_info', - defaultMessage: - 'This account privacy status is set to locked. The owner manually reviews who can follow them.', - }, + editProfile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, mention: { id: 'account.mention', defaultMessage: 'Mention' }, chat: { id: 'account.chat', defaultMessage: 'Chat with @{name}' }, direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' }, @@ -78,9 +69,7 @@ const messages = defineMessages({ hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide reposts from @{name}' }, showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show reposts from @{name}' }, preferences: { id: 'column.preferences', defaultMessage: 'Preferences' }, - follow_requests: { id: 'column.follow_requests', defaultMessage: 'Follow requests' }, blocks: { id: 'column.blocks', defaultMessage: 'Blocks' }, - domain_blocks: { id: 'column.domain_blocks', defaultMessage: 'Domain blocks' }, mutes: { id: 'column.mutes', defaultMessage: 'Mutes' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: "Don't feature on profile" }, @@ -90,7 +79,7 @@ const messages = defineMessages({ defaultMessage: 'Remove this follower', }, adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, - add_or_remove_from_list: { + addOrRemoveFromList: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or remove from lists', }, @@ -496,7 +485,7 @@ const Header: React.FC = ({ account }) => { if (account.id === ownAccount.id) { menu.push({ - text: intl.formatMessage(messages.edit_profile), + text: intl.formatMessage(messages.editProfile), to: '/settings/profile', icon: require('@phosphor-icons/core/regular/user.svg'), }); @@ -548,7 +537,7 @@ const Header: React.FC = ({ account }) => { if (features.lists) { menu.push({ - text: intl.formatMessage(messages.add_or_remove_from_list), + text: intl.formatMessage(messages.addOrRemoveFromList), action: onAddToList, icon: require('@phosphor-icons/core/regular/list-bullets.svg'), }); @@ -567,7 +556,7 @@ const Header: React.FC = ({ account }) => { } } else if (features.lists && features.unrestrictedLists) { menu.push({ - text: intl.formatMessage(messages.add_or_remove_from_list), + text: intl.formatMessage(messages.addOrRemoveFromList), action: onAddToList, icon: require('@phosphor-icons/core/regular/list-bullets.svg'), }); diff --git a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx index bae9bab6b..4388c07c2 100644 --- a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx +++ b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx @@ -25,10 +25,6 @@ import type { CreateAccountParams } from 'pl-api'; const messages = defineMessages({ username: { id: 'registration.fields.username_placeholder', defaultMessage: 'Username' }, - username_hint: { - id: 'registration.fields.username_hint', - defaultMessage: 'Only letters, numbers, and underscores are allowed.', - }, usernameUnavailable: { id: 'registration.username_unavailable', defaultMessage: 'Username is already taken.', @@ -40,18 +36,6 @@ const messages = defineMessages({ defaultMessage: "Passwords don't match.", }, confirm: { id: 'registration.fields.confirm_placeholder', defaultMessage: 'Password (again)' }, - agreement: { id: 'registration.agreement', defaultMessage: 'I agree to the {tos}.' }, - tos: { id: 'registration.tos', defaultMessage: 'Terms of Service' }, - close: { id: 'registration.confirmation_modal.close', defaultMessage: 'Close' }, - newsletter: { id: 'registration.newsletter', defaultMessage: 'Subscribe to newsletter.' }, - needsConfirmationHeader: { - id: 'confirmations.register.needs_confirmation.header', - defaultMessage: 'Confirmation needed', - }, - needsApprovalHeader: { - id: 'confirmations.register.needs_approval.header', - defaultMessage: 'Approval needed', - }, reasonHint: { id: 'registration.reason_hint', defaultMessage: 'This will help us review your application', @@ -183,13 +167,21 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { ); openModal('CONFIRM', { - heading: needsConfirmation - ? intl.formatMessage(messages.needsConfirmationHeader) - : needsApproval - ? intl.formatMessage(messages.needsApprovalHeader) - : undefined, + heading: needsConfirmation ? ( + + ) : needsApproval ? ( + + ) : undefined, message, - confirm: intl.formatMessage(messages.close), + confirm: ( + + ), onConfirm: () => {}, }); }; @@ -287,7 +279,12 @@ const RegistrationForm: React.FC = ({ inviteToken }) => {
<> + } errors={ usernameUnavailable ? [intl.formatMessage(messages.usernameUnavailable)] : undefined } @@ -400,13 +397,19 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { /> - {intl.formatMessage(messages.tos)} - - ), - })} + labelText={ + + + + ), + }} + /> + } > = ({ composeId, compact }) if (postFormats.includes('text/plain')) { options.push({ icon: require('@phosphor-icons/core/regular/paragraph.svg'), - text: intl.formatMessage(messages.content_type_plaintext), + text: intl.formatMessage(messages.contentTypePlaintext), value: 'text/plain', }); } @@ -59,7 +59,7 @@ const ContentTypeButton: React.FC = ({ composeId, compact }) if (postFormats.includes('text/markdown')) { options.push({ icon: require('@phosphor-icons/core/regular/markdown-logo.svg'), - text: intl.formatMessage(messages.content_type_markdown), + text: intl.formatMessage(messages.contentTypeMarkdown), value: 'text/markdown', }); } @@ -67,7 +67,7 @@ const ContentTypeButton: React.FC = ({ composeId, compact }) if (postFormats.includes('text/x.misskeymarkdown')) { options.push({ icon: require('@phosphor-icons/core/regular/sparkle.svg'), - text: intl.formatMessage(messages.content_type_mfm), + text: intl.formatMessage(messages.contentTypeMfm), value: 'text/x.misskeymarkdown', }); } @@ -75,7 +75,7 @@ const ContentTypeButton: React.FC = ({ composeId, compact }) if (postFormats.includes('text/html')) { options.push({ icon: require('@phosphor-icons/core/regular/file-html.svg'), - text: intl.formatMessage(messages.content_type_html), + text: intl.formatMessage(messages.contentTypeHtml), value: 'text/html', }); } @@ -83,7 +83,7 @@ const ContentTypeButton: React.FC = ({ composeId, compact }) if (postFormats.includes('text/markdown')) { options.push({ icon: require('@phosphor-icons/core/regular/text-indent.svg'), - text: intl.formatMessage(messages.content_type_wysiwyg), + text: intl.formatMessage(messages.contentTypeWysiwyg), value: 'wysiwyg', }); } @@ -101,7 +101,7 @@ const ContentTypeButton: React.FC = ({ composeId, compact }) > )} diff --git a/packages/pl-fe/src/features/compose/components/schedule-button.tsx b/packages/pl-fe/src/features/compose/components/schedule-button.tsx index 6fa40a0c1..606e0fb11 100644 --- a/packages/pl-fe/src/features/compose/components/schedule-button.tsx +++ b/packages/pl-fe/src/features/compose/components/schedule-button.tsx @@ -8,8 +8,8 @@ import { useCompose } from '@/hooks/use-compose'; import ComposeFormButton from './compose-form-button'; const messages = defineMessages({ - add_schedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' }, - remove_schedule: { id: 'schedule_button.remove_schedule', defaultMessage: 'Post immediately' }, + addSchedule: { id: 'schedule_button.add_schedule', defaultMessage: 'Schedule post for later' }, + removeSchedule: { id: 'schedule_button.remove_schedule', defaultMessage: 'Post immediately' }, }); interface IScheduleButton { @@ -41,7 +41,7 @@ const ScheduleButton: React.FC = ({ composeId, disabled }) => { return ( = ({ }; const getI18n = () => ({ - search: intl.formatMessage(messages.emoji_search), - pick: intl.formatMessage(messages.emoji_pick), - search_no_results_1: intl.formatMessage(messages.emoji_oh_no), - search_no_results_2: intl.formatMessage(messages.emoji_not_found), - add_custom: intl.formatMessage(messages.emoji_add_custom), + search: intl.formatMessage(messages.emojiSearch), + pick: intl.formatMessage(messages.emojiPick), + search_no_results_1: intl.formatMessage(messages.emojiOhNo), + search_no_results_2: intl.formatMessage(messages.emojiNotFound), + add_custom: intl.formatMessage(messages.emojiAddCustom), categories: { - search: intl.formatMessage(messages.search_results), + search: intl.formatMessage(messages.searchResults), frequent: intl.formatMessage(messages.recent), people: intl.formatMessage(messages.people), nature: intl.formatMessage(messages.nature), @@ -195,13 +195,13 @@ const EmojiPickerDropdown: React.FC = ({ custom: intl.formatMessage(messages.custom), }, skins: { - choose: intl.formatMessage(messages.skins_choose), - 1: intl.formatMessage(messages.skins_1), - 2: intl.formatMessage(messages.skins_2), - 3: intl.formatMessage(messages.skins_3), - 4: intl.formatMessage(messages.skins_4), - 5: intl.formatMessage(messages.skins_5), - 6: intl.formatMessage(messages.skins_6), + choose: intl.formatMessage(messages.skinsChoose), + 1: intl.formatMessage(messages.skins1), + 2: intl.formatMessage(messages.skins2), + 3: intl.formatMessage(messages.skins3), + 4: intl.formatMessage(messages.skins4), + 5: intl.formatMessage(messages.skins5), + 6: intl.formatMessage(messages.skins6), }, }); diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index 2cb247b8a..c12f67d8a 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -50,17 +50,17 @@ const messages = defineMessages({ unbookmark: { id: 'status.unbookmark', defaultMessage: 'Remove bookmark' }, quotePost: { id: 'event.quote', defaultMessage: 'Quote event' }, reblog: { id: 'event.reblog', defaultMessage: 'Repost event' }, - reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, - cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, - reblog_visibility_public: { + reblogPrivate: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, + cancelReblogPrivate: { id: 'status.cancel_reblog_private', defaultMessage: 'Un-repost' }, + reblogVisibilityPublic: { id: 'status.reblog_visibility_public', defaultMessage: 'Public repost', }, - reblog_visibility_unlisted: { + reblogVisibilityUnlisted: { id: 'status.reblog_visibility_unlisted', defaultMessage: 'Quiet public repost', }, - reblog_visibility_private: { + reblogVisibilityPrivate: { id: 'status.reblog_visibility_private', defaultMessage: 'Followers-only repost', }, @@ -88,12 +88,6 @@ const messages = defineMessages({ defaultMessage: 'Mark post not sensitive', }, deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' }, - deleteConfirm: { id: 'confirmations.delete_event.confirm', defaultMessage: 'Delete' }, - deleteHeading: { id: 'confirmations.delete_event.heading', defaultMessage: 'Delete event' }, - deleteMessage: { - id: 'confirmations.delete_event.message', - defaultMessage: 'Are you sure you want to delete this event?', - }, }); interface IEventHeader { @@ -203,9 +197,16 @@ const EventHeader: React.FC = ({ status }) => { const handleDeleteClick = () => { openModal('CONFIRM', { - heading: intl.formatMessage(messages.deleteHeading), - message: intl.formatMessage(messages.deleteMessage), - confirm: intl.formatMessage(messages.deleteConfirm), + heading: ( + + ), + message: ( + + ), + confirm: , onConfirm: () => dispatch(deleteStatus(status.id)), }); }; @@ -288,21 +289,21 @@ const EventHeader: React.FC = ({ status }) => { ? { items: [ { - text: intl.formatMessage(messages.reblog_visibility_public), + text: intl.formatMessage(messages.reblogVisibilityPublic), action: () => { handleReblogClick('public'); }, icon: require('@phosphor-icons/core/regular/globe.svg'), }, { - text: intl.formatMessage(messages.reblog_visibility_unlisted), + text: intl.formatMessage(messages.reblogVisibilityUnlisted), action: () => { handleReblogClick('unlisted'); }, icon: require('@phosphor-icons/core/regular/moon.svg'), }, { - text: intl.formatMessage(messages.reblog_visibility_private), + text: intl.formatMessage(messages.reblogVisibilityPrivate), action: () => { handleReblogClick('private'); }, @@ -328,7 +329,7 @@ const EventHeader: React.FC = ({ status }) => { } else if (status.visibility === 'private' || status.visibility === 'mutuals_only') { menu.push({ text: intl.formatMessage( - status.reblogged ? messages.cancel_reblog_private : messages.reblog_private, + status.reblogged ? messages.cancelReblogPrivate : messages.reblogPrivate, ), action: () => { handleReblogClick(); diff --git a/packages/pl-fe/src/features/preferences/index.tsx b/packages/pl-fe/src/features/preferences/index.tsx index 7215c3c4f..99c6bc309 100644 --- a/packages/pl-fe/src/features/preferences/index.tsx +++ b/packages/pl-fe/src/features/preferences/index.tsx @@ -108,22 +108,22 @@ const messages = defineMessages({ id: 'preferences.fields.display_media.show_all', defaultMessage: 'Always show posts', }, - privacy_public: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' }, - privacy_unlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' }, - privacy_followers_only: { + privacyPublic: { id: 'preferences.options.privacy_public', defaultMessage: 'Public' }, + privacyUnlisted: { id: 'preferences.options.privacy_unlisted', defaultMessage: 'Unlisted' }, + privacyFollowersOnly: { id: 'preferences.options.privacy_followers_only', defaultMessage: 'Followers-only', }, - content_type_plaintext: { + contentTypePlaintext: { id: 'preferences.options.content_type_plaintext', defaultMessage: 'Plain text', }, - content_type_markdown: { + contentTypeMarkdown: { id: 'preferences.options.content_type_markdown', defaultMessage: 'Markdown', }, - content_type_html: { id: 'preferences.options.content_type_html', defaultMessage: 'HTML' }, - content_type_wysiwyg: { + contentTypeHtml: { id: 'preferences.options.content_type_html', defaultMessage: 'HTML' }, + contentTypeWysiwyg: { id: 'preferences.options.content_type_wysiwyg', defaultMessage: 'WYSIWYG', }, @@ -234,9 +234,9 @@ const Preferences = () => { const defaultPrivacyOptions = React.useMemo( () => ({ - public: intl.formatMessage(messages.privacy_public), - unlisted: intl.formatMessage(messages.privacy_unlisted), - private: intl.formatMessage(messages.privacy_followers_only), + public: intl.formatMessage(messages.privacyPublic), + unlisted: intl.formatMessage(messages.privacyUnlisted), + private: intl.formatMessage(messages.privacyFollowersOnly), }), [settings.locale], ); @@ -268,13 +268,13 @@ const Preferences = () => { const postFormats = instance.pleroma.metadata.post_formats; const options = Object.entries({ - 'text/plain': intl.formatMessage(messages.content_type_plaintext), - 'text/markdown': intl.formatMessage(messages.content_type_markdown), - 'text/html': intl.formatMessage(messages.content_type_html), + 'text/plain': intl.formatMessage(messages.contentTypePlaintext), + 'text/markdown': intl.formatMessage(messages.contentTypeMarkdown), + 'text/html': intl.formatMessage(messages.contentTypeHtml), }).filter(([key]) => postFormats.includes(key)); if (postFormats.includes('text/markdown')) - options.push(['wysiwyg', intl.formatMessage(messages.content_type_wysiwyg)]); + options.push(['wysiwyg', intl.formatMessage(messages.contentTypeWysiwyg)]); if (options.length > 1) return Object.fromEntries(options); }, [settings.locale]); diff --git a/packages/pl-fe/src/features/security/mfa/disable-otp-form.tsx b/packages/pl-fe/src/features/security/mfa/disable-otp-form.tsx index 544e0a958..3241e63f4 100644 --- a/packages/pl-fe/src/features/security/mfa/disable-otp-form.tsx +++ b/packages/pl-fe/src/features/security/mfa/disable-otp-form.tsx @@ -14,8 +14,7 @@ import { useDisableMfa } from '@/queries/security/use-mfa'; import toast from '@/toast'; const messages = defineMessages({ - mfa_setup_disable_button: { id: 'column.mfa_disable_button', defaultMessage: 'Disable' }, - disableFail: { id: 'security.disable.fail', defaultMessage: 'Incorrect password. Try again.' }, + disableFail: { id: 'mfa.disable.fail', defaultMessage: 'Incorrect password. Try again.' }, mfaDisableSuccess: { id: 'mfa.disable.success_message', defaultMessage: 'MFA disabled' }, passwordPlaceholder: { id: 'mfa.mfa_setup.password_placeholder', defaultMessage: 'Password' }, }); @@ -85,7 +84,7 @@ const DisableOtpForm: React.FC = () => { disabled={isPending} theme='danger' type='submit' - text={intl.formatMessage(messages.mfa_setup_disable_button)} + text={} /> diff --git a/packages/pl-fe/src/features/ui/components/action-button.tsx b/packages/pl-fe/src/features/ui/components/action-button.tsx index 2a52c2013..9cafc2203 100644 --- a/packages/pl-fe/src/features/ui/components/action-button.tsx +++ b/packages/pl-fe/src/features/ui/components/action-button.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { defineMessages, useIntl } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Button from '@/components/ui/button'; import HStack from '@/components/ui/hstack'; @@ -25,20 +25,6 @@ import toast from '@/toast'; import type { Account } from 'pl-api'; const messages = defineMessages({ - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - blocked: { id: 'account.blocked', defaultMessage: 'Blocked' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - follow: { id: 'account.follow', defaultMessage: 'Follow' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - remote_follow: { id: 'account.remote_follow', defaultMessage: 'Remote follow' }, - requested: { id: 'account.requested', defaultMessage: 'Follow requested. Click to cancel' }, - requested_small: { id: 'account.requested_small', defaultMessage: 'Follow requested' }, - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' }, - reject: { id: 'follow_request.reject', defaultMessage: 'Reject' }, - bite: { id: 'account.bite', defaultMessage: 'Bite @{name}' }, userBit: { id: 'account.bite.success', defaultMessage: 'You have bit @{acct}' }, userBiteFail: { id: 'account.bite.fail', defaultMessage: 'Failed to bite @{acct}' }, }); @@ -134,25 +120,54 @@ const ActionButton: React.FC = ({ account, actionType, small = tr /** Handles actionType='muting' */ const mutingAction = () => { const isMuted = relationship?.muting; - const messageKey = isMuted ? messages.unmute : messages.mute; - const text = intl.formatMessage(messageKey, { name: account.username }); return ( - ); } else if (relationship?.blocking) { @@ -298,7 +330,13 @@ const ActionButton: React.FC = ({ account, actionType, small = tr
)} - + + } + />
diff --git a/packages/pl-fe/src/pages/settings/export-data.tsx b/packages/pl-fe/src/pages/settings/export-data.tsx index b3e5a449f..2ac80913d 100644 --- a/packages/pl-fe/src/pages/settings/export-data.tsx +++ b/packages/pl-fe/src/pages/settings/export-data.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { defineMessages, useIntl, type MessageDescriptor } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { exportFollows, exportBlocks, exportMutes } from '@/actions/export-data'; import Button from '@/components/ui/button'; @@ -12,17 +12,14 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import type { AppDispatch, RootState } from '@/store'; interface ICSVExporter { - messages: { - input_label: MessageDescriptor; - input_hint: MessageDescriptor; - submit: MessageDescriptor; - }; + inputLabel: React.ReactNode; + inputHint: React.ReactNode; + submitText: React.ReactNode; action: () => (dispatch: AppDispatch, getState: () => RootState) => Promise; } -const CSVExporter: React.FC = ({ messages, action }) => { +const CSVExporter: React.FC = ({ inputLabel, inputHint, submitText, action }) => { const dispatch = useAppDispatch(); - const intl = useIntl(); const [isLoading, setIsLoading] = useState(false); @@ -40,13 +37,13 @@ const CSVExporter: React.FC = ({ messages, action }) => { return (
- {intl.formatMessage(messages.input_label)} + {inputLabel} - {intl.formatMessage(messages.input_hint)} + {inputHint}
@@ -58,41 +55,53 @@ const messages = defineMessages({ submit: { id: 'export_data.actions.export', defaultMessage: 'Export' }, }); -const followMessages = defineMessages({ - input_label: { id: 'export_data.follows_label', defaultMessage: 'Follows' }, - input_hint: { - id: 'export_data.hints.follows', - defaultMessage: 'Get a CSV file containing a list of followed accounts', - }, - submit: { id: 'export_data.actions.export_follows', defaultMessage: 'Export follows' }, -}); - -const blockMessages = defineMessages({ - input_label: { id: 'export_data.blocks_label', defaultMessage: 'Blocks' }, - input_hint: { - id: 'export_data.hints.blocks', - defaultMessage: 'Get a CSV file containing a list of blocked accounts', - }, - submit: { id: 'export_data.actions.export_blocks', defaultMessage: 'Export blocks' }, -}); - -const muteMessages = defineMessages({ - input_label: { id: 'export_data.mutes_label', defaultMessage: 'Mutes' }, - input_hint: { - id: 'export_data.hints.mutes', - defaultMessage: 'Get a CSV file containing a list of muted accounts', - }, - submit: { id: 'export_data.actions.export_mutes', defaultMessage: 'Export mutes' }, -}); - const ExportDataPage = () => { const intl = useIntl(); return ( - - - + } + inputHint={ + + } + submitText={ + + } + /> + } + inputHint={ + + } + submitText={ + + } + /> + } + inputHint={ + + } + submitText={ + + } + /> ); }; diff --git a/packages/pl-fe/src/pages/settings/import-data.tsx b/packages/pl-fe/src/pages/settings/import-data.tsx index fd6ffc058..788eb2943 100644 --- a/packages/pl-fe/src/pages/settings/import-data.tsx +++ b/packages/pl-fe/src/pages/settings/import-data.tsx @@ -1,6 +1,6 @@ import { serialize } from 'object-to-formdata'; import React, { useState } from 'react'; -import { defineMessages, FormattedMessage, useIntl, type MessageDescriptor } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import List, { ListItem } from '@/components/list'; import Button from '@/components/ui/button'; @@ -33,61 +33,23 @@ const messages = defineMessages({ }, }); -const followMessages = defineMessages({ - input_label: { id: 'import_data.follows_label', defaultMessage: 'Follows' }, - input_hint: { - id: 'import_data.hints.follows', - defaultMessage: 'CSV file containing a list of followed accounts', - }, - submit: { id: 'import_data.actions.import_follows', defaultMessage: 'Import follows' }, -}); - -const blockMessages = defineMessages({ - input_label: { id: 'import_data.blocks_label', defaultMessage: 'Blocks' }, - input_hint: { - id: 'import_data.hints.blocks', - defaultMessage: 'CSV file containing a list of blocked accounts', - }, - submit: { id: 'import_data.actions.import_blocks', defaultMessage: 'Import blocks' }, -}); - -const muteMessages = defineMessages({ - input_label: { id: 'import_data.mutes_label', defaultMessage: 'Mutes' }, - input_hint: { - id: 'import_data.hints.mutes', - defaultMessage: 'CSV file containing a list of muted accounts', - }, - submit: { id: 'import_data.actions.import_mutes', defaultMessage: 'Import mutes' }, -}); - -const archiveMessages = defineMessages({ - input_label: { id: 'import_data.archive_label', defaultMessage: 'Archive' }, - input_hint: { - id: 'import_data.hints.archive', - defaultMessage: 'Archive containing an archive of statuses', - }, - submit: { id: 'import_data.actions.import_archive', defaultMessage: 'Import archive' }, -}); - interface IDataImporter { - messages: { - input_label: MessageDescriptor; - input_hint: MessageDescriptor; - submit: MessageDescriptor; - }; + inputLabel: React.ReactNode; + inputHint: React.ReactNode; + submitText: React.ReactNode; action: (list: File, overwrite?: boolean) => Promise; accept?: string; allowOverwrite?: boolean; } const DataImporter: React.FC = ({ - messages, + inputLabel, + inputHint, + submitText, action, accept = '.csv,text/csv', allowOverwrite, }) => { - const intl = useIntl(); - const [isLoading, setIsLoading] = useState(false); const [file, setFile] = useState(null); const [overwrite, setOverwrite] = useState(false); @@ -113,9 +75,9 @@ const DataImporter: React.FC = ({ return (
- {intl.formatMessage(messages.input_label)} + {messages.inputLabel} - {intl.formatMessage(messages.input_hint)}}> + {messages.inputHint}}> @@ -141,7 +103,7 @@ const DataImporter: React.FC = ({ @@ -187,28 +149,73 @@ const ImportDataPage = () => { {features.importFollows && ( } + inputHint={ + + } + submitText={ + + } allowOverwrite={features.importOverwrite} /> )} {features.importBlocks && ( } + inputHint={ + + } + submitText={ + + } allowOverwrite={features.importOverwrite} /> )} {features.importMutes && ( } + inputHint={ + + } + submitText={ + + } allowOverwrite={features.importOverwrite} /> )} {features.importArchive && ( } + inputHint={ + + } + submitText={ + + } accept='.tar,.tar.gz,.zip' /> )} diff --git a/packages/pl-fe/src/pages/settings/interaction-policies.tsx b/packages/pl-fe/src/pages/settings/interaction-policies.tsx index dd4581e9f..fab840ed6 100644 --- a/packages/pl-fe/src/pages/settings/interaction-policies.tsx +++ b/packages/pl-fe/src/pages/settings/interaction-policies.tsx @@ -35,17 +35,11 @@ const messages = defineMessages({ public: { id: 'interaction_policies.tabs.public', defaultMessage: 'Public' }, unlisted: { id: 'interaction_policies.tabs.unlisted', defaultMessage: 'Unlisted' }, private: { id: 'interaction_policies.tabs.private', defaultMessage: 'Followers-only' }, - submit: { id: 'interaction_policies.update', defaultMessage: 'Update' }, success: { id: 'interaction_policies.success', defaultMessage: 'Updated interaction policies' }, fail: { id: 'interaction_policies.fail', defaultMessage: 'Failed to update interaction policies', }, - always: { id: 'interaction_policies.rule.always', defaultMessage: 'Always' }, - with_approval: { - id: 'interaction_policies.rule.with_approval', - defaultMessage: 'Require approval', - }, }); const scopeMessages = defineMessages({ @@ -206,7 +200,14 @@ const InteractionPolicyConfig: React.FC = ({ )} - + + } + > items={items} value={interactionPolicy[policy].always as Array} @@ -214,7 +215,14 @@ const InteractionPolicyConfig: React.FC = ({ disabled={disabled} /> - + + } + > } @@ -369,7 +377,7 @@ const InteractionPoliciesPage = () => { From cec1c206f2e6ec10145de682b9f993355be437cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 12:32:14 +0100 Subject: [PATCH 041/264] nicolium: rename reducer i'll probably remove soon lol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/push-notifications/registerer.ts | 4 ++-- packages/pl-fe/src/reducers/index.ts | 4 ++-- packages/pl-fe/src/reducers/push-notifications.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/pl-fe/src/actions/push-notifications/registerer.ts b/packages/pl-fe/src/actions/push-notifications/registerer.ts index 1f6c6a83b..ed502ea7d 100644 --- a/packages/pl-fe/src/actions/push-notifications/registerer.ts +++ b/packages/pl-fe/src/actions/push-notifications/registerer.ts @@ -51,7 +51,7 @@ const unsubscribe = ({ const sendSubscriptionToBackend = (subscription: PushSubscription, me: Me) => (dispatch: AppDispatch, getState: () => RootState) => { - const alerts = getState().push_notifications.alerts; + const alerts = getState().pushNotifications.alerts; const params = { subscription, data: { alerts } }; if (me) { @@ -96,7 +96,7 @@ const register = () => (dispatch: AppDispatch, getState: () => RootState) => { subscription.options.applicationServerKey!, ).toString(); const subscriptionServerKey = urlBase64ToUint8Array(vapidKey).toString(); - const serverEndpoint = getState().push_notifications.subscription?.endpoint; + const serverEndpoint = getState().pushNotifications.subscription?.endpoint; // If the VAPID public key did not change and the endpoint corresponds // to the endpoint saved in the backend, the subscription is valid diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 91aa36555..d44c4423b 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -12,7 +12,7 @@ import frontendConfig from './frontend-config'; import instance from './instance'; import me from './me'; import meta from './meta'; -import push_notifications from './push-notifications'; +import pushNotifications from './push-notifications'; import statuses from './statuses'; import timelines from './timelines'; @@ -26,7 +26,7 @@ const reducers = { instance, me, meta, - push_notifications, + pushNotifications, statuses, timelines, }; diff --git a/packages/pl-fe/src/reducers/push-notifications.ts b/packages/pl-fe/src/reducers/push-notifications.ts index 509107d31..88aba091a 100644 --- a/packages/pl-fe/src/reducers/push-notifications.ts +++ b/packages/pl-fe/src/reducers/push-notifications.ts @@ -35,7 +35,7 @@ const initialState: State = { browserSupport: false, }; -const push_subscriptions = (state = initialState, action: SetterAction): State => { +const pushSubscriptions = (state = initialState, action: SetterAction): State => { switch (action.type) { case SET_SUBSCRIPTION: return create(state, (draft) => { @@ -57,4 +57,4 @@ const push_subscriptions = (state = initialState, action: SetterAction): State = } }; -export { push_subscriptions as default }; +export { pushSubscriptions as default }; From 2b9f73e09a893c727c4df11a9fc9a9f39b4e8b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 12:39:34 +0100 Subject: [PATCH 042/264] nicolium: well MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/status-action-bar.tsx | 12 ++++++------ packages/pl-fe/src/modals/boost-modal.tsx | 2 +- packages/pl-fe/src/pages/settings/import-data.tsx | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index e6c59bb02..8140a836d 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -135,20 +135,20 @@ const messages = defineMessages({ pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, quotePost: { id: 'status.quote', defaultMessage: 'Quote post' }, reblog: { id: 'status.reblog', defaultMessage: 'Repost' }, - reblog_private: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, - reblog_visibility: { + reblogPrivate: { id: 'status.reblog_private', defaultMessage: 'Repost to original audience' }, + reblogVisibility: { id: 'status.reblog_visibility', defaultMessage: 'Repost to specific audience', }, - reblog_visibility_public: { + reblogVisibilityPublic: { id: 'status.reblog_visibility_public', defaultMessage: 'Public repost', }, - reblog_visibility_unlisted: { + reblogVisibilityUnlisted: { id: 'status.reblog_visibility_unlisted', defaultMessage: 'Quiet public repost', }, - reblog_visibility_private: { + reblogVisibilityPrivate: { id: 'status.reblog_visibility_private', defaultMessage: 'Followers-only repost', }, @@ -161,7 +161,7 @@ const messages = defineMessages({ defaultMessage: 'Are you sure you want to delete this post and re-draft it? Favorites and reposts will be lost, and replies to the original post will be orphaned.', }, - replies_disabled_group: { + repliesDisabledGroup: { id: 'status.disabled_replies.group_membership', defaultMessage: 'Only group members can reply', }, diff --git a/packages/pl-fe/src/modals/boost-modal.tsx b/packages/pl-fe/src/modals/boost-modal.tsx index 7d68e9852..0d9ee5477 100644 --- a/packages/pl-fe/src/modals/boost-modal.tsx +++ b/packages/pl-fe/src/modals/boost-modal.tsx @@ -50,7 +50,7 @@ const BoostModal: React.FC = ({ ) } confirmationAction={handleReblog} - confirmationText={intl.formatMessage(buttonText)} + confirmationText={buttonText} > diff --git a/packages/pl-fe/src/pages/settings/import-data.tsx b/packages/pl-fe/src/pages/settings/import-data.tsx index 788eb2943..94599cbfd 100644 --- a/packages/pl-fe/src/pages/settings/import-data.tsx +++ b/packages/pl-fe/src/pages/settings/import-data.tsx @@ -75,9 +75,9 @@ const DataImporter: React.FC = ({ return (
- {messages.inputLabel} + {inputLabel} - {messages.inputHint}}> + {inputHint}}> @@ -103,7 +103,7 @@ const DataImporter: React.FC = ({ From 6668ea4402ee6708cdbd1f4b3d9201e11f976feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 12:50:01 +0100 Subject: [PATCH 043/264] nicolium: update en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 7ac6d87dd..17fbbd0e8 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -1144,7 +1144,7 @@ "import_data.archive_label": "Archive", "import_data.blocks_label": "Blocks", "import_data.follows_label": "Follows", - "import_data.hints.archive": "File containing your archive data", + "import_data.hints.archive": "File containing an archive of statuses", "import_data.hints.blocks": "CSV file containing a list of blocked accounts", "import_data.hints.follows": "CSV file containing a list of followed accounts", "import_data.hints.mutes": "CSV file containing a list of muted accounts", @@ -1166,7 +1166,7 @@ "interaction_policies.mentioned_warning": "Mentioned users can always reply.", "interaction_policies.preferences_hint": "Control, who can interact with this post. You can also configure the default interaction policies in Preferences > Interaction policies.", "interaction_policies.rule.always": "Always", - "interaction_policies.rule.with_approval": "With approval", + "interaction_policies.rule.with_approval": "Require approval", "interaction_policies.success": "Updated interaction policies", "interaction_policies.tabs.private": "Followers-only", "interaction_policies.tabs.public": "Public", From 83b205724e31b2d77cd543a4e5770b8dc8abd308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 12:58:22 +0100 Subject: [PATCH 044/264] nicolium: update some deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/package.json | 28 +- pnpm-lock.yaml | 836 ++++++++++++++++++++++-------------- 2 files changed, 535 insertions(+), 329 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index 9c1189433..d1576ee3f 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -60,15 +60,15 @@ "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-pacer": "^0.16.4", - "@tanstack/react-query": "^5.90.16", - "@tanstack/react-router": "^1.145.7", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-router": "^1.162.8", "@transfem-org/sfm-js": "^0.24.6", "@twemoji/svg": "^15.0.0", "@uidotdev/usehooks": "^2.4.1", "@use-gesture/react": "^10.3.1", "@yornaath/batshit": "^0.11.2", "abortcontroller-polyfill": "^1.7.8", - "autoprefixer": "^10.4.23", + "autoprefixer": "^10.4.24", "blurhash": "^2.0.5", "bowser": "^2.13.1", "browserslist": "^4.28.1", @@ -86,12 +86,12 @@ "flexsearch": "^0.7.43", "fuzzysort": "^3.1.0", "graphemesplit": "^2.4.4", - "html-react-parser": "^5.2.11", + "html-react-parser": "^5.2.17", "intersection-observer": "^0.12.2", "intl-messageformat": "^10.7.18", "intl-pluralrules": "^2.0.1", "isomorphic-dompurify": "^2.35.0", - "leaflet": "^1.8.0", + "leaflet": "^1.9.4", "lexical": "^0.39.0", "line-awesome": "^1.3.0", "localforage": "^1.10.0", @@ -106,10 +106,10 @@ "punycode": "^2.1.1", "qrcode.react": "^4.2.0", "query-string": "^9.3.1", - "react": "^19.2.3", + "react": "^19.2.4", "react-color": "^2.19.3", "react-datepicker": "^8.3.0", - "react-dom": "^19.2.3", + "react-dom": "^19.2.4", "react-helmet-async": "^2.0.5", "react-hot-toast": "^2.6.0", "react-inlinesvg": "^4.1.8", @@ -129,15 +129,15 @@ "use-mutative": "^1.3.1", "util": "^0.12.5", "valibot": "^1.2.0", - "zustand": "^5.0.9", + "zustand": "^5.0.11", "zustand-mutative": "^1.3.1" }, "devDependencies": { - "@formatjs/cli": "^6.9.0", + "@formatjs/cli": "^6.13.0", "@sentry/types": "^8.47.0", "@types/dom-chromium-ai": "^0.0.11", - "@types/leaflet": "^1.9.15", - "@types/lodash": "^4.17.13", + "@types/leaflet": "^1.9.21", + "@types/lodash": "^4.17.14", "@types/path-browserify": "^1.0.3", "@types/react": "^19.2.7", "@types/react-color": "^3.0.13", @@ -148,9 +148,9 @@ "@vitejs/plugin-react": "^5.1.3", "eslint-plugin-formatjs": "^5.4.2", "globals": "^15.14.0", - "oxfmt": "^0.32.0", + "oxfmt": "^0.35.0", "oxlint": "^1.47.0", - "oxlint-tsgolint": "^0.12.2", + "oxlint-tsgolint": "^0.14.2", "rollup-plugin-bundle-stats": "^4.21.10", "stylelint": "^16.12.0", "stylelint-config-standard-scss": "^12.0.0", @@ -158,7 +158,7 @@ "tslib": "^2.8.1", "type-fest": "^4.30.1", "typescript": "5.7.3", - "vite": "^7.0.0", + "vite": "^7.3.1", "vite-plugin-checker": "^0.12.0", "vite-plugin-compile-time": "^0.4.6", "vite-plugin-html": "^3.2.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8735e7ccd..6964e7e1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: version: 0.32.0 oxlint: specifier: ^1.47.0 - version: 1.47.0(oxlint-tsgolint@0.12.2) + version: 1.47.0(oxlint-tsgolint@0.14.2) typedoc: specifier: ^0.28.7 version: 0.28.9(typescript@5.9.2) @@ -102,7 +102,7 @@ importers: version: 1.2.1 '@floating-ui/react': specifier: ^0.27.16 - version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@fontsource/inter': specifier: ^5.2.8 version: 5.2.8 @@ -132,7 +132,7 @@ importers: version: 0.39.0 '@lexical/react': specifier: ^0.39.0 - version: 0.39.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(yjs@13.6.27) + version: 0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27) '@lexical/rich-text': specifier: ^0.39.0 version: 0.39.0 @@ -150,25 +150,25 @@ importers: version: 2.1.1 '@reach/combobox': specifier: ^0.18.0 - version: 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@reach/rect': specifier: ^0.18.0 - version: 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@reach/tabs': specifier: ^0.18.0 - version: 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@react-spring/web': specifier: ^10.0.3 - version: 10.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@reduxjs/toolkit': specifier: ^2.5.0 - version: 2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.3)(redux@5.0.1))(react@19.2.3) + version: 2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@sentry/browser': specifier: ^8.47.0 version: 8.55.0 '@sentry/react': specifier: ^8.47.0 - version: 8.55.0(react@19.2.3) + version: 8.55.0(react@19.2.4) '@tailwindcss/aspect-ratio': specifier: ^0.4.2 version: 0.4.2(tailwindcss@3.4.17) @@ -180,13 +180,13 @@ importers: version: 0.5.16(tailwindcss@3.4.17) '@tanstack/react-pacer': specifier: ^0.16.4 - version: 0.16.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 0.16.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': - specifier: ^5.90.16 - version: 5.90.16(react@19.2.3) + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) '@tanstack/react-router': - specifier: ^1.145.7 - version: 1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^1.162.8 + version: 1.162.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@transfem-org/sfm-js': specifier: ^0.24.6 version: 0.24.8 @@ -195,10 +195,10 @@ importers: version: 15.0.0 '@uidotdev/usehooks': specifier: ^2.4.1 - version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@use-gesture/react': specifier: ^10.3.1 - version: 10.3.1(react@19.2.3) + version: 10.3.1(react@19.2.4) '@yornaath/batshit': specifier: ^0.11.2 version: 0.11.2 @@ -206,8 +206,8 @@ importers: specifier: ^1.7.8 version: 1.7.8 autoprefixer: - specifier: ^10.4.23 - version: 10.4.23(postcss@8.5.6) + specifier: ^10.4.24 + version: 10.4.24(postcss@8.5.6) blurhash: specifier: ^2.0.5 version: 2.0.5 @@ -260,8 +260,8 @@ importers: specifier: ^2.4.4 version: 2.6.0 html-react-parser: - specifier: ^5.2.11 - version: 5.2.11(@types/react@18.3.27)(react@19.2.3) + specifier: ^5.2.17 + version: 5.2.17(@types/react@18.3.27)(react@19.2.4) intersection-observer: specifier: ^0.12.2 version: 0.12.2 @@ -275,7 +275,7 @@ importers: specifier: ^2.35.0 version: 2.35.0 leaflet: - specifier: ^1.8.0 + specifier: ^1.9.4 version: 1.9.4 lexical: specifier: ^0.39.0 @@ -315,49 +315,49 @@ importers: version: 2.3.1 qrcode.react: specifier: ^4.2.0 - version: 4.2.0(react@19.2.3) + version: 4.2.0(react@19.2.4) query-string: specifier: ^9.3.1 version: 9.3.1 react: - specifier: ^19.2.3 - version: 19.2.3 + specifier: ^19.2.4 + version: 19.2.4 react-color: specifier: ^2.19.3 - version: 2.19.3(react@19.2.3) + version: 2.19.3(react@19.2.4) react-datepicker: specifier: ^8.3.0 - version: 8.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 8.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) + specifier: ^19.2.4 + version: 19.2.4(react@19.2.4) react-helmet-async: specifier: ^2.0.5 - version: 2.0.5(react@19.2.3) + version: 2.0.5(react@19.2.4) react-hot-toast: specifier: ^2.6.0 - version: 2.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-inlinesvg: specifier: ^4.1.8 - version: 4.2.0(react@19.2.3) + version: 4.2.0(react@19.2.4) react-intl: specifier: ^8.0.10 - version: 8.0.10(@types/react@18.3.27)(react@19.2.3)(typescript@5.7.3) + version: 8.0.10(@types/react@18.3.27)(react@19.2.4)(typescript@5.7.3) react-redux: specifier: ^9.0.4 - version: 9.2.0(@types/react@18.3.27)(react@19.2.3)(redux@5.0.1) + version: 9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1) react-sparklines: specifier: ^1.7.0 - version: 1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 1.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react-sticky-box: specifier: ^2.0.5 - version: 2.0.5(react@19.2.3) + version: 2.0.5(react@19.2.4) react-swipeable-views: specifier: ^0.14.0 - version: 0.14.0(react@19.2.3) + version: 0.14.0(react@19.2.4) react-virtuoso: specifier: ^4.18.1 - version: 4.18.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 4.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) redux: specifier: ^5.0.1 version: 5.0.1 @@ -381,7 +381,7 @@ importers: version: 6.4.0 use-mutative: specifier: ^1.3.1 - version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.3) + version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4) util: specifier: ^0.12.5 version: 0.12.5 @@ -389,15 +389,15 @@ importers: specifier: ^1.2.0 version: 1.2.0(typescript@5.7.3) zustand: - specifier: ^5.0.9 - version: 5.0.9(@types/react@18.3.27)(immer@10.1.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) + specifier: ^5.0.11 + version: 5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) zustand-mutative: specifier: ^1.3.1 - version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.3)(zustand@5.0.9(@types/react@18.3.27)(immer@10.1.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))) + version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) devDependencies: '@formatjs/cli': - specifier: ^6.9.0 - version: 6.9.0(@vue/compiler-core@3.5.18) + specifier: ^6.13.0 + version: 6.13.0 '@sentry/types': specifier: ^8.47.0 version: 8.55.0 @@ -405,10 +405,10 @@ importers: specifier: ^0.0.11 version: 0.0.11 '@types/leaflet': - specifier: ^1.9.15 - version: 1.9.20 + specifier: ^1.9.21 + version: 1.9.21 '@types/lodash': - specifier: ^4.17.13 + specifier: ^4.17.14 version: 4.17.20 '@types/path-browserify': specifier: ^1.0.3 @@ -441,14 +441,14 @@ importers: specifier: ^15.14.0 version: 15.15.0 oxfmt: - specifier: ^0.32.0 - version: 0.32.0 + specifier: ^0.35.0 + version: 0.35.0 oxlint: specifier: ^1.47.0 - version: 1.47.0(oxlint-tsgolint@0.12.2) + version: 1.47.0(oxlint-tsgolint@0.14.2) oxlint-tsgolint: - specifier: ^0.12.2 - version: 0.12.2 + specifier: ^0.14.2 + version: 0.14.2 rollup-plugin-bundle-stats: specifier: ^4.21.10 version: 4.21.10(core-js@3.44.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) @@ -471,11 +471,11 @@ importers: specifier: 5.7.3 version: 5.7.3 vite: - specifier: ^7.0.0 + specifier: ^7.3.1 version: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) vite-plugin-checker: specifier: ^0.12.0 - version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.12.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) vite-plugin-compile-time: specifier: ^0.4.6 version: 0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) @@ -1588,19 +1588,18 @@ packages: '@fontsource/tajawal@5.2.7': resolution: {integrity: sha512-EzswJ4JoFmYQ7hn7O5Do7Azd1tjzujSEzN+DvTZNcAwZ3usfPvEJ3hTQtYv9VN+mUH4flOi6LedtqCkVUHZs9A==} - '@formatjs/cli@6.9.0': - resolution: {integrity: sha512-gYzzsYvff8RoIZZczepjd0z+lmrVMjBKaVaHg39GCOJfDvBNnS96kHGwHqRn2NfbOEv5eqYZQL5liYSXHgKqtQ==} - engines: {node: '>= 16'} + '@formatjs/cli@6.13.0': + resolution: {integrity: sha512-bl4+FNg7S6RPNa9cSAE8HqdXu84n7LpzDdkDAPqS0sk58XNbY/1Le6GdWqCKzELWX+FhI58gyZtZecmWsZ+Bhg==} + engines: {node: '>= 20.12.0'} hasBin: true peerDependencies: '@glimmer/env': '*' '@glimmer/reference': '*' - '@glimmer/syntax': ^0.95.0 + '@glimmer/syntax': ^0.84.3 || ^0.95.0 '@glimmer/validator': '*' - '@vue/compiler-core': ^3.5.12 - content-tag: '4' - ember-template-recast: ^6.1.5 - vue: ^3.5.12 + '@vue/compiler-core': 3.5.27 + content-tag: ^4.1.0 + vue: 3.5.27 peerDependenciesMeta: '@glimmer/env': optional: true @@ -1614,8 +1613,6 @@ packages: optional: true content-tag: optional: true - ember-template-recast: - optional: true vue: optional: true @@ -1863,141 +1860,255 @@ packages: cpu: [arm] os: [android] + '@oxfmt/binding-android-arm-eabi@0.35.0': + resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxfmt/binding-android-arm64@0.32.0': resolution: {integrity: sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxfmt/binding-android-arm64@0.35.0': + resolution: {integrity: sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxfmt/binding-darwin-arm64@0.32.0': resolution: {integrity: sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxfmt/binding-darwin-arm64@0.35.0': + resolution: {integrity: sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxfmt/binding-darwin-x64@0.32.0': resolution: {integrity: sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxfmt/binding-darwin-x64@0.35.0': + resolution: {integrity: sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxfmt/binding-freebsd-x64@0.32.0': resolution: {integrity: sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxfmt/binding-freebsd-x64@0.35.0': + resolution: {integrity: sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': resolution: {integrity: sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': + resolution: {integrity: sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.32.0': resolution: {integrity: sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.35.0': + resolution: {integrity: sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm64-gnu@0.32.0': resolution: {integrity: sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@oxfmt/binding-linux-arm64-gnu@0.35.0': + resolution: {integrity: sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@oxfmt/binding-linux-arm64-musl@0.32.0': resolution: {integrity: sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@oxfmt/binding-linux-arm64-musl@0.35.0': + resolution: {integrity: sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@oxfmt/binding-linux-ppc64-gnu@0.32.0': resolution: {integrity: sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + '@oxfmt/binding-linux-ppc64-gnu@0.35.0': + resolution: {integrity: sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + '@oxfmt/binding-linux-riscv64-gnu@0.32.0': resolution: {integrity: sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + '@oxfmt/binding-linux-riscv64-gnu@0.35.0': + resolution: {integrity: sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + '@oxfmt/binding-linux-riscv64-musl@0.32.0': resolution: {integrity: sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + '@oxfmt/binding-linux-riscv64-musl@0.35.0': + resolution: {integrity: sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + '@oxfmt/binding-linux-s390x-gnu@0.32.0': resolution: {integrity: sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + '@oxfmt/binding-linux-s390x-gnu@0.35.0': + resolution: {integrity: sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + '@oxfmt/binding-linux-x64-gnu@0.32.0': resolution: {integrity: sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@oxfmt/binding-linux-x64-gnu@0.35.0': + resolution: {integrity: sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@oxfmt/binding-linux-x64-musl@0.32.0': resolution: {integrity: sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@oxfmt/binding-linux-x64-musl@0.35.0': + resolution: {integrity: sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@oxfmt/binding-openharmony-arm64@0.32.0': resolution: {integrity: sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxfmt/binding-openharmony-arm64@0.35.0': + resolution: {integrity: sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxfmt/binding-win32-arm64-msvc@0.32.0': resolution: {integrity: sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@oxfmt/binding-win32-arm64-msvc@0.35.0': + resolution: {integrity: sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.32.0': resolution: {integrity: sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.35.0': + resolution: {integrity: sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxfmt/binding-win32-x64-msvc@0.32.0': resolution: {integrity: sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint-tsgolint/darwin-arm64@0.12.2': - resolution: {integrity: sha512-XIfavTqkJPGYi/98z7ZCkZvXq2AccMAAB0iwvKDRTQqiweMXVUyeUdx46phCHHH1PgmIVJtVfysThkHq2xCyrw==} + '@oxfmt/binding-win32-x64-msvc@0.35.0': + resolution: {integrity: sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@oxlint-tsgolint/darwin-arm64@0.14.2': + resolution: {integrity: sha512-03WxIXguCXf1pTmoG2C6vqRcbrU9GaJCW6uTIiQdIQq4BrJnVWZv99KEUQQRkuHK78lOLa9g7B4K58NcVcB54g==} cpu: [arm64] os: [darwin] - '@oxlint-tsgolint/darwin-x64@0.12.2': - resolution: {integrity: sha512-tytsvP6zmNShRNDo4GgQartOXmd4GPd+TylCUMdO/iWl9PZVOgRyswWbYVTNgn85Cib/aY2q3Uu+jOw+QlbxvQ==} + '@oxlint-tsgolint/darwin-x64@0.14.2': + resolution: {integrity: sha512-ksMLl1cIWz3Jw+U79BhyCPdvohZcJ/xAKri5bpT6oeEM2GVnQCHBk/KZKlYrd7hZUTxz0sLnnKHE11XFnLASNQ==} cpu: [x64] os: [darwin] - '@oxlint-tsgolint/linux-arm64@0.12.2': - resolution: {integrity: sha512-3W38yJuF7taEquhEuD6mYQyCeWNAlc1pNPjFkspkhLKZVgbrhDA4V6fCxLDDRvrTHde0bXPmFvuPlUq5pSePgA==} + '@oxlint-tsgolint/linux-arm64@0.14.2': + resolution: {integrity: sha512-2BgR535w7GLxBCyQD5DR3dBzbAgiBbG5QX1kAEVzOmWxJhhGxt5lsHdHebRo7ilukYLpBDkerz0mbMErblghCQ==} cpu: [arm64] os: [linux] - '@oxlint-tsgolint/linux-x64@0.12.2': - resolution: {integrity: sha512-EjcEspeeV0NmaopEp4wcN5ntQP9VCJJDrTvzOjMP4W6ajz18M+pni9vkKvmcPIpRa/UmWobeFgKoVd/KGueeuQ==} + '@oxlint-tsgolint/linux-x64@0.14.2': + resolution: {integrity: sha512-TUHFyVHfbbGtnTQZbUFgwvv3NzXBgzNLKdMUJw06thpiC7u5OW5qdk4yVXIC/xeVvdl3NAqTfcT4sA32aiMubg==} cpu: [x64] os: [linux] - '@oxlint-tsgolint/win32-arm64@0.12.2': - resolution: {integrity: sha512-a9L7iA5K/Ht/i8d9+7RTp6hbPa4cyXP0MdySVXAO6vczpL/4ildfY9Hr2m2wqL12uK6xe/uVABpVTrqay/wV+g==} + '@oxlint-tsgolint/win32-arm64@0.14.2': + resolution: {integrity: sha512-OfYHa/irfVggIFEC4TbawsI7Hwrttppv//sO/e00tu4b2QRga7+VHAwtCkSFWSr0+BsO4InRYVA0+pun5BinpQ==} cpu: [arm64] os: [win32] - '@oxlint-tsgolint/win32-x64@0.12.2': - resolution: {integrity: sha512-Cvt40UbTf5ib12DjGN+mMGOnjWa4Bc6Y7KEaXXp9qzckvs3HpNk2wSwMV3gnuR8Ipx4hkzkzrgzD0BAUsySAfA==} + '@oxlint-tsgolint/win32-x64@0.14.2': + resolution: {integrity: sha512-5gxwbWYE2pP+pzrO4SEeYvLk4N609eAe18rVXUx+en3qtHBkU8VM2jBmMcZdIHn+G05leu4pYvwAvw6tvT9VbA==} cpu: [x64] os: [win32] @@ -2673,9 +2784,9 @@ packages: resolution: {integrity: sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg==} engines: {node: '>=18'} - '@tanstack/history@1.145.7': - resolution: {integrity: sha512-gMo/ReTUp0a3IOcZoI3hH6PLDC2R/5ELQ7P2yu9F6aEkA0wSQh+Q4qzMrtcKvF2ut0oE+16xWCGDo/TdYd6cEQ==} - engines: {node: '>=12'} + '@tanstack/history@1.161.4': + resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} + engines: {node: '>=20.19'} '@tanstack/pacer@0.15.4': resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==} @@ -2684,8 +2795,8 @@ packages: '@tanstack/query-core@5.83.1': resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} - '@tanstack/query-core@5.90.16': - resolution: {integrity: sha512-MvtWckSVufs/ja463/K4PyJeqT+HMlJWtw6PrCpywznd2NSgO3m4KwO9RqbFqGg6iDE8vVMFWMeQI4Io3eEYww==} + '@tanstack/query-core@5.90.20': + resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} '@tanstack/react-pacer@0.16.4': resolution: {integrity: sha512-nuQLE8bx0rYMiJau4jOTPZFp3XC/GnIHDKfKVVWeKUHNF4grRdVHPgTlJ8EV/nt/HJxSUnIcy+IIKX+Bj0bLSw==} @@ -2699,14 +2810,14 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-query@5.90.16': - resolution: {integrity: sha512-bpMGOmV4OPmif7TNMteU/Ehf/hoC0Kf98PDc0F4BZkFrEapRMEqI/V6YS0lyzwSV6PQpY1y4xxArUIfBW5LVxQ==} + '@tanstack/react-query@5.90.21': + resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: react: ^18 || ^19 - '@tanstack/react-router@1.145.7': - resolution: {integrity: sha512-0O+a4TjJSPXd2BsvDPwDPBKRQKYqNIBg5TAg9NzCteqJ0NXRxwohyqCksHqCEEtJe/uItwqmHoqkK4q5MDhEsA==} - engines: {node: '>=12'} + '@tanstack/react-router@1.162.8': + resolution: {integrity: sha512-WunoknGI5ielJ833yl/F7Vq4nv/OWzrJVBsMgyxX16Db1DwVvX/B5zTg8EMjdZUOJ7ONpvur3t4aq7KQiYRagQ==} + engines: {node: '>=20.19'} peerDependencies: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -2717,21 +2828,21 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/react-store@0.8.0': - resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==} + '@tanstack/react-store@0.9.1': + resolution: {integrity: sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/router-core@1.145.7': - resolution: {integrity: sha512-v6jx6JqVUBM0/FcBq1tX22xiPq8Ufc0PDEP582/4deYoq2/RYd+bZstANp3mGSsqdxE/luhoLYuuSQiwi/j1wA==} - engines: {node: '>=12'} + '@tanstack/router-core@1.162.6': + resolution: {integrity: sha512-WFMNysDsDtnlM0G0L4LPWJuvpGatlPvBLGlPnieWYKem/Ed4mRHu7Hqw78MR/CMuFSRi9Gvv91/h8F3EVswAJw==} + engines: {node: '>=20.19'} '@tanstack/store@0.7.7': resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} - '@tanstack/store@0.8.0': - resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==} + '@tanstack/store@0.9.1': + resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} '@transfem-org/sfm-js@0.24.8': resolution: {integrity: sha1-G97++XwNPZZaxIExiJbm2kJZSg0=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.8.tgz} @@ -2808,8 +2919,8 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/leaflet@1.9.20': - resolution: {integrity: sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==} + '@types/leaflet@1.9.21': + resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} '@types/lodash.omit@4.5.9': resolution: {integrity: sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw==} @@ -3347,8 +3458,8 @@ packages: resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} engines: {node: '>= 4.0.0'} - autoprefixer@10.4.23: - resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==} + autoprefixer@10.4.24: + resolution: {integrity: sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -3471,6 +3582,9 @@ packages: caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} + caniuse-lite@1.0.30001774: + resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -3865,6 +3979,10 @@ packages: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -4326,8 +4444,8 @@ packages: hookified@1.11.0: resolution: {integrity: sha512-aDdIN3GyU5I6wextPplYdfmWCo+aLmjjVbntmX6HLD5RCi/xKsivYEBhnRD+d9224zFf008ZpLMPlWF0ZodYZw==} - html-dom-parser@5.1.2: - resolution: {integrity: sha512-9nD3Rj3/FuQt83AgIa1Y3ruzspwFFA54AJbQnohXN+K6fL1/bhcDQJJY5Ne4L4A163ADQFVESd/0TLyNoV0mfg==} + html-dom-parser@5.1.8: + resolution: {integrity: sha512-MCIUng//mF2qTtGHXJWr6OLfHWmg3Pm8ezpfiltF83tizPWY17JxT4dRLE8lykJ5bChJELoY3onQKPbufJHxYA==} html-encoding-sniffer@6.0.0: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} @@ -4338,8 +4456,8 @@ packages: engines: {node: '>=12'} hasBin: true - html-react-parser@5.2.11: - resolution: {integrity: sha512-WnSQVn/D1UTj64nSz5y8MriL+MrbsZH80Ytr1oqKqs8DGZnphWY1R1pl3t7TY3rpqTSu+FHA21P80lrsmrdNBA==} + html-react-parser@5.2.17: + resolution: {integrity: sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w==} peerDependencies: '@types/react': ^18.3.18 react: 0.14 || 15 || 16 || 17 || 18 || 19 @@ -4351,8 +4469,8 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} - htmlparser2@10.0.0: - resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} http-link-header@1.1.3: resolution: {integrity: sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==} @@ -5150,8 +5268,13 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - oxlint-tsgolint@0.12.2: - resolution: {integrity: sha512-IFiOhYZfSgiHbBznTZOhFpEHpsZFSP0j7fVRake03HEkgH0YljnTFDNoRkGWsTrnrHr7nRIomSsF4TnCI/O+kQ==} + oxfmt@0.35.0: + resolution: {integrity: sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + oxlint-tsgolint@0.14.2: + resolution: {integrity: sha512-XJsFIQwnYJgXFlNDz2MncQMWYxwnfy4BCy73mdiFN/P13gEZrAfBU4Jmz2XXFf9UG0wPILdi7hYa6t0KmKQLhw==} hasBin: true oxlint@1.47.0: @@ -5578,10 +5701,10 @@ packages: react: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc - react-dom@19.2.3: - resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} + react-dom@19.2.4: + resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: - react: ^19.2.3 + react: ^19.2.4 react-error-boundary@6.0.0: resolution: {integrity: sha512-gdlJjD7NWr0IfkPlaREN2d9uUZUlksrfOx7SX62VRerwXbMY6ftGCIZua1VG1aXFNOimhISsTq+Owp725b9SiA==} @@ -5685,8 +5808,8 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} - react@19.2.3: - resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} + react@19.2.4: + resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} reactcss@1.2.3: @@ -6971,8 +7094,8 @@ packages: react: ^18.0 || ^17.0 || ^19.0 zustand: ^4.0 || ^5.0 - zustand@5.0.9: - resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + zustand@5.0.11: + resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} peerDependencies: '@types/react': ^18.3.18 @@ -8031,18 +8154,18 @@ snapshots: '@floating-ui/core': 1.7.3 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@floating-ui/react-dom@2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@floating-ui/dom': 1.7.4 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@floating-ui/react@0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@floating-ui/react@0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) tabbable: 6.4.0 '@floating-ui/utils@0.2.10': {} @@ -8055,9 +8178,7 @@ snapshots: '@fontsource/tajawal@5.2.7': {} - '@formatjs/cli@6.9.0(@vue/compiler-core@3.5.18)': - optionalDependencies: - '@vue/compiler-core': 3.5.18 + '@formatjs/cli@6.13.0': {} '@formatjs/ecma402-abstract@2.3.6': dependencies: @@ -8151,9 +8272,9 @@ snapshots: '@humanwhocodes/object-schema@2.0.3': {} - '@icons/material@0.2.4(react@19.2.3)': + '@icons/material@0.2.4(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 '@isaacs/balanced-match@4.0.1': {} @@ -8227,7 +8348,7 @@ snapshots: lexical: 0.39.0 prismjs: 1.30.0 - '@lexical/devtools-core@0.39.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@lexical/devtools-core@0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@lexical/html': 0.39.0 '@lexical/link': 0.39.0 @@ -8235,8 +8356,8 @@ snapshots: '@lexical/table': 0.39.0 '@lexical/utils': 0.39.0 lexical: 0.39.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@lexical/dragon@0.39.0': dependencies: @@ -8311,10 +8432,10 @@ snapshots: '@lexical/utils': 0.39.0 lexical: 0.39.0 - '@lexical/react@0.39.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(yjs@13.6.27)': + '@lexical/react@0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27)': dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@lexical/devtools-core': 0.39.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react': 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/devtools-core': 0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@lexical/dragon': 0.39.0 '@lexical/extension': 0.39.0 '@lexical/hashtag': 0.39.0 @@ -8331,9 +8452,9 @@ snapshots: '@lexical/utils': 0.39.0 '@lexical/yjs': 0.39.0(yjs@13.6.27) lexical: 0.39.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-error-boundary: 6.0.0(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-error-boundary: 6.0.0(react@19.2.4) transitivePeerDependencies: - yjs @@ -8476,76 +8597,133 @@ snapshots: '@oxfmt/binding-android-arm-eabi@0.32.0': optional: true + '@oxfmt/binding-android-arm-eabi@0.35.0': + optional: true + '@oxfmt/binding-android-arm64@0.32.0': optional: true + '@oxfmt/binding-android-arm64@0.35.0': + optional: true + '@oxfmt/binding-darwin-arm64@0.32.0': optional: true + '@oxfmt/binding-darwin-arm64@0.35.0': + optional: true + '@oxfmt/binding-darwin-x64@0.32.0': optional: true + '@oxfmt/binding-darwin-x64@0.35.0': + optional: true + '@oxfmt/binding-freebsd-x64@0.32.0': optional: true + '@oxfmt/binding-freebsd-x64@0.35.0': + optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': + optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.32.0': optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.35.0': + optional: true + '@oxfmt/binding-linux-arm64-gnu@0.32.0': optional: true + '@oxfmt/binding-linux-arm64-gnu@0.35.0': + optional: true + '@oxfmt/binding-linux-arm64-musl@0.32.0': optional: true + '@oxfmt/binding-linux-arm64-musl@0.35.0': + optional: true + '@oxfmt/binding-linux-ppc64-gnu@0.32.0': optional: true + '@oxfmt/binding-linux-ppc64-gnu@0.35.0': + optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.32.0': optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.35.0': + optional: true + '@oxfmt/binding-linux-riscv64-musl@0.32.0': optional: true + '@oxfmt/binding-linux-riscv64-musl@0.35.0': + optional: true + '@oxfmt/binding-linux-s390x-gnu@0.32.0': optional: true + '@oxfmt/binding-linux-s390x-gnu@0.35.0': + optional: true + '@oxfmt/binding-linux-x64-gnu@0.32.0': optional: true + '@oxfmt/binding-linux-x64-gnu@0.35.0': + optional: true + '@oxfmt/binding-linux-x64-musl@0.32.0': optional: true + '@oxfmt/binding-linux-x64-musl@0.35.0': + optional: true + '@oxfmt/binding-openharmony-arm64@0.32.0': optional: true + '@oxfmt/binding-openharmony-arm64@0.35.0': + optional: true + '@oxfmt/binding-win32-arm64-msvc@0.32.0': optional: true + '@oxfmt/binding-win32-arm64-msvc@0.35.0': + optional: true + '@oxfmt/binding-win32-ia32-msvc@0.32.0': optional: true + '@oxfmt/binding-win32-ia32-msvc@0.35.0': + optional: true + '@oxfmt/binding-win32-x64-msvc@0.32.0': optional: true - '@oxlint-tsgolint/darwin-arm64@0.12.2': + '@oxfmt/binding-win32-x64-msvc@0.35.0': optional: true - '@oxlint-tsgolint/darwin-x64@0.12.2': + '@oxlint-tsgolint/darwin-arm64@0.14.2': optional: true - '@oxlint-tsgolint/linux-arm64@0.12.2': + '@oxlint-tsgolint/darwin-x64@0.14.2': optional: true - '@oxlint-tsgolint/linux-x64@0.12.2': + '@oxlint-tsgolint/linux-arm64@0.14.2': optional: true - '@oxlint-tsgolint/win32-arm64@0.12.2': + '@oxlint-tsgolint/linux-x64@0.14.2': optional: true - '@oxlint-tsgolint/win32-x64@0.12.2': + '@oxlint-tsgolint/win32-arm64@0.14.2': + optional: true + + '@oxlint-tsgolint/win32-x64@0.14.2': optional: true '@oxlint/binding-android-arm-eabi@1.47.0': @@ -8673,105 +8851,105 @@ snapshots: '@preact/signals-core@1.12.1': {} - '@reach/auto-id@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/auto-id@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reach/combobox@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/combobox@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/auto-id': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/descendants': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/polymorphic': 0.18.0(react@19.2.3) - '@reach/popover': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/portal': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/auto-id': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/descendants': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/polymorphic': 0.18.0(react@19.2.4) + '@reach/popover': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/portal': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reach/descendants@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/descendants@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@reach/observe-rect@1.2.0': {} - '@reach/polymorphic@0.18.0(react@19.2.3)': + '@reach/polymorphic@0.18.0(react@19.2.4)': dependencies: - react: 19.2.3 + react: 19.2.4 - '@reach/popover@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/popover@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/polymorphic': 0.18.0(react@19.2.3) - '@reach/portal': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/rect': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/polymorphic': 0.18.0(react@19.2.4) + '@reach/portal': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/rect': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) tabbable: 5.3.3 - '@reach/portal@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/portal@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reach/rect@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/rect@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@reach/observe-rect': 1.2.0 - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reach/tabs@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/tabs@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@reach/auto-id': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/descendants': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@reach/polymorphic': 0.18.0(react@19.2.3) - '@reach/utils': 0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@reach/auto-id': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/descendants': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reach/polymorphic': 0.18.0(react@19.2.4) + '@reach/utils': 0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reach/utils@0.18.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@reach/utils@0.18.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@react-spring/animated@10.0.3(react@19.2.3)': + '@react-spring/animated@10.0.3(react@19.2.4)': dependencies: - '@react-spring/shared': 10.0.3(react@19.2.3) + '@react-spring/shared': 10.0.3(react@19.2.4) '@react-spring/types': 10.0.3 - react: 19.2.3 + react: 19.2.4 - '@react-spring/core@10.0.3(react@19.2.3)': + '@react-spring/core@10.0.3(react@19.2.4)': dependencies: - '@react-spring/animated': 10.0.3(react@19.2.3) - '@react-spring/shared': 10.0.3(react@19.2.3) + '@react-spring/animated': 10.0.3(react@19.2.4) + '@react-spring/shared': 10.0.3(react@19.2.4) '@react-spring/types': 10.0.3 - react: 19.2.3 + react: 19.2.4 '@react-spring/rafz@10.0.3': {} - '@react-spring/shared@10.0.3(react@19.2.3)': + '@react-spring/shared@10.0.3(react@19.2.4)': dependencies: '@react-spring/rafz': 10.0.3 '@react-spring/types': 10.0.3 - react: 19.2.3 + react: 19.2.4 '@react-spring/types@10.0.3': {} - '@react-spring/web@10.0.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@react-spring/web@10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@react-spring/animated': 10.0.3(react@19.2.3) - '@react-spring/core': 10.0.3(react@19.2.3) - '@react-spring/shared': 10.0.3(react@19.2.3) + '@react-spring/animated': 10.0.3(react@19.2.4) + '@react-spring/core': 10.0.3(react@19.2.4) + '@react-spring/shared': 10.0.3(react@19.2.4) '@react-spring/types': 10.0.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 @@ -8780,8 +8958,8 @@ snapshots: redux-thunk: 3.1.0(redux@5.0.1) reselect: 5.1.1 optionalDependencies: - react: 19.2.3 - react-redux: 9.2.0(@types/react@18.3.27)(react@19.2.3)(redux@5.0.1) + react: 19.2.4 + react-redux: 9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1) '@rolldown/binding-android-arm64@1.0.0-rc.4': optional: true @@ -9059,12 +9237,12 @@ snapshots: '@sentry/core@8.55.0': {} - '@sentry/react@8.55.0(react@19.2.3)': + '@sentry/react@8.55.0(react@19.2.4)': dependencies: '@sentry/browser': 8.55.0 '@sentry/core': 8.55.0 hoist-non-react-statics: 3.3.2 - react: 19.2.3 + react: 19.2.4 '@sentry/types@8.55.0': dependencies: @@ -9120,7 +9298,7 @@ snapshots: '@tanstack/devtools-event-client@0.3.3': {} - '@tanstack/history@1.145.7': {} + '@tanstack/history@1.161.4': {} '@tanstack/pacer@0.15.4': dependencies: @@ -9129,54 +9307,54 @@ snapshots: '@tanstack/query-core@5.83.1': {} - '@tanstack/query-core@5.90.16': {} + '@tanstack/query-core@5.90.20': {} - '@tanstack/react-pacer@0.16.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-pacer@0.16.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/pacer': 0.15.4 - '@tanstack/react-store': 0.7.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@tanstack/react-store': 0.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@tanstack/react-query@5.84.1(react@18.3.1)': dependencies: '@tanstack/query-core': 5.83.1 react: 18.3.1 - '@tanstack/react-query@5.90.16(react@19.2.3)': + '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: - '@tanstack/query-core': 5.90.16 - react: 19.2.3 + '@tanstack/query-core': 5.90.20 + react: 19.2.4 - '@tanstack/react-router@1.145.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-router@1.162.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/history': 1.145.7 - '@tanstack/react-store': 0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/router-core': 1.145.7 + '@tanstack/history': 1.161.4 + '@tanstack/react-store': 0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/router-core': 1.162.6 isbot: 5.1.32 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-store@0.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: '@tanstack/store': 0.7.7 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - use-sync-external-store: 1.5.0(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.5.0(react@19.2.4) - '@tanstack/react-store@0.8.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@tanstack/react-store@0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/store': 0.8.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - use-sync-external-store: 1.6.0(react@19.2.3) + '@tanstack/store': 0.9.1 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) - '@tanstack/router-core@1.145.7': + '@tanstack/router-core@1.162.6': dependencies: - '@tanstack/history': 1.145.7 - '@tanstack/store': 0.8.0 + '@tanstack/history': 1.161.4 + '@tanstack/store': 0.9.1 cookie-es: 2.0.0 seroval: 1.4.2 seroval-plugins: 1.4.2(seroval@1.4.2) @@ -9185,7 +9363,7 @@ snapshots: '@tanstack/store@0.7.7': {} - '@tanstack/store@0.8.0': {} + '@tanstack/store@0.9.1': {} '@transfem-org/sfm-js@0.24.8': dependencies: @@ -9269,7 +9447,7 @@ snapshots: '@types/json5@0.0.29': {} - '@types/leaflet@1.9.20': + '@types/leaflet@1.9.21': dependencies: '@types/geojson': 7946.0.16 @@ -9490,10 +9668,10 @@ snapshots: '@typescript-eslint/types': 8.38.0 eslint-visitor-keys: 4.2.1 - '@uidotdev/usehooks@2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@uidotdev/usehooks@2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) '@ungap/structured-clone@1.3.0': {} @@ -9558,10 +9736,10 @@ snapshots: '@use-gesture/core@10.3.1': {} - '@use-gesture/react@10.3.1(react@19.2.3)': + '@use-gesture/react@10.3.1(react@19.2.4)': dependencies: '@use-gesture/core': 10.3.1 - react: 19.2.3 + react: 19.2.4 '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0))': dependencies: @@ -9884,10 +10062,10 @@ snapshots: at-least-node@1.0.0: {} - autoprefixer@10.4.23(postcss@8.5.6): + autoprefixer@10.4.24(postcss@8.5.6): dependencies: browserslist: 4.28.1 - caniuse-lite: 1.0.30001762 + caniuse-lite: 1.0.30001774 fraction.js: 5.3.4 picocolors: 1.1.1 postcss: 8.5.6 @@ -10025,6 +10203,8 @@ snapshots: caniuse-lite@1.0.30001762: {} + caniuse-lite@1.0.30001774: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -10406,6 +10586,8 @@ snapshots: entities@6.0.1: {} + entities@7.0.1: {} + env-paths@2.2.1: {} environment@1.1.0: {} @@ -11033,10 +11215,10 @@ snapshots: hookified@1.11.0: {} - html-dom-parser@5.1.2: + html-dom-parser@5.1.8: dependencies: domhandler: 5.0.3 - htmlparser2: 10.0.0 + htmlparser2: 10.1.0 html-encoding-sniffer@6.0.0: dependencies: @@ -11054,11 +11236,11 @@ snapshots: relateurl: 0.2.7 terser: 5.43.1 - html-react-parser@5.2.11(@types/react@18.3.27)(react@19.2.3): + html-react-parser@5.2.17(@types/react@18.3.27)(react@19.2.4): dependencies: domhandler: 5.0.3 - html-dom-parser: 5.1.2 - react: 19.2.3 + html-dom-parser: 5.1.8 + react: 19.2.4 react-property: 2.0.2 style-to-js: 1.1.21 optionalDependencies: @@ -11066,12 +11248,12 @@ snapshots: html-tags@3.3.1: {} - htmlparser2@10.0.0: + htmlparser2@10.1.0: dependencies: domelementtype: 2.3.0 domhandler: 5.0.3 domutils: 3.2.2 - entities: 6.0.1 + entities: 7.0.1 http-link-header@1.1.3: {} @@ -11853,16 +12035,40 @@ snapshots: '@oxfmt/binding-win32-ia32-msvc': 0.32.0 '@oxfmt/binding-win32-x64-msvc': 0.32.0 - oxlint-tsgolint@0.12.2: + oxfmt@0.35.0: + dependencies: + tinypool: 2.1.0 optionalDependencies: - '@oxlint-tsgolint/darwin-arm64': 0.12.2 - '@oxlint-tsgolint/darwin-x64': 0.12.2 - '@oxlint-tsgolint/linux-arm64': 0.12.2 - '@oxlint-tsgolint/linux-x64': 0.12.2 - '@oxlint-tsgolint/win32-arm64': 0.12.2 - '@oxlint-tsgolint/win32-x64': 0.12.2 + '@oxfmt/binding-android-arm-eabi': 0.35.0 + '@oxfmt/binding-android-arm64': 0.35.0 + '@oxfmt/binding-darwin-arm64': 0.35.0 + '@oxfmt/binding-darwin-x64': 0.35.0 + '@oxfmt/binding-freebsd-x64': 0.35.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.35.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.35.0 + '@oxfmt/binding-linux-arm64-gnu': 0.35.0 + '@oxfmt/binding-linux-arm64-musl': 0.35.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.35.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.35.0 + '@oxfmt/binding-linux-riscv64-musl': 0.35.0 + '@oxfmt/binding-linux-s390x-gnu': 0.35.0 + '@oxfmt/binding-linux-x64-gnu': 0.35.0 + '@oxfmt/binding-linux-x64-musl': 0.35.0 + '@oxfmt/binding-openharmony-arm64': 0.35.0 + '@oxfmt/binding-win32-arm64-msvc': 0.35.0 + '@oxfmt/binding-win32-ia32-msvc': 0.35.0 + '@oxfmt/binding-win32-x64-msvc': 0.35.0 - oxlint@1.47.0(oxlint-tsgolint@0.12.2): + oxlint-tsgolint@0.14.2: + optionalDependencies: + '@oxlint-tsgolint/darwin-arm64': 0.14.2 + '@oxlint-tsgolint/darwin-x64': 0.14.2 + '@oxlint-tsgolint/linux-arm64': 0.14.2 + '@oxlint-tsgolint/linux-x64': 0.14.2 + '@oxlint-tsgolint/win32-arm64': 0.14.2 + '@oxlint-tsgolint/win32-x64': 0.14.2 + + oxlint@1.47.0(oxlint-tsgolint@0.14.2): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.47.0 '@oxlint/binding-android-arm64': 1.47.0 @@ -11883,7 +12089,7 @@ snapshots: '@oxlint/binding-win32-arm64-msvc': 1.47.0 '@oxlint/binding-win32-ia32-msvc': 1.47.0 '@oxlint/binding-win32-x64-msvc': 1.47.0 - oxlint-tsgolint: 0.12.2 + oxlint-tsgolint: 0.14.2 p-limit@2.3.0: dependencies: @@ -12213,9 +12419,9 @@ snapshots: punycode@2.3.1: {} - qrcode.react@4.2.0(react@19.2.3): + qrcode.react@4.2.0(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 quansync@0.2.10: {} @@ -12237,68 +12443,68 @@ snapshots: dependencies: safe-buffer: 5.2.1 - react-color@2.19.3(react@19.2.3): + react-color@2.19.3(react@19.2.4): dependencies: - '@icons/material': 0.2.4(react@19.2.3) + '@icons/material': 0.2.4(react@19.2.4) lodash: 4.17.23 lodash-es: 4.17.23 material-colors: 1.2.6 prop-types: 15.8.1 - react: 19.2.3 - reactcss: 1.2.3(react@19.2.3) + react: 19.2.4 + reactcss: 1.2.3(react@19.2.4) tinycolor2: 1.6.0 - react-datepicker@8.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-datepicker@8.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@floating-ui/react': 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) clsx: 2.1.1 date-fns: 4.1.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-dom@19.2.3(react@19.2.3): + react-dom@19.2.4(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 scheduler: 0.27.0 - react-error-boundary@6.0.0(react@19.2.3): + react-error-boundary@6.0.0(react@19.2.4): dependencies: '@babel/runtime': 7.28.6 - react: 19.2.3 + react: 19.2.4 - react-event-listener@0.6.6(react@19.2.3): + react-event-listener@0.6.6(react@19.2.4): dependencies: '@babel/runtime': 7.28.2 prop-types: 15.8.1 - react: 19.2.3 + react: 19.2.4 warning: 4.0.3 react-fast-compare@3.2.2: {} - react-from-dom@0.7.5(react@19.2.3): + react-from-dom@0.7.5(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 - react-helmet-async@2.0.5(react@19.2.3): + react-helmet-async@2.0.5(react@19.2.4): dependencies: invariant: 2.2.4 - react: 19.2.3 + react: 19.2.4 react-fast-compare: 3.2.2 shallowequal: 1.1.0 - react-hot-toast@2.6.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-hot-toast@2.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: csstype: 3.1.3 goober: 2.1.16(csstype@3.1.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-inlinesvg@4.2.0(react@19.2.3): + react-inlinesvg@4.2.0(react@19.2.4): dependencies: - react: 19.2.3 - react-from-dom: 0.7.5(react@19.2.3) + react: 19.2.4 + react-from-dom: 0.7.5(react@19.2.4) - react-intl@8.0.10(@types/react@18.3.27)(react@19.2.3)(typescript@5.7.3): + react-intl@8.0.10(@types/react@18.3.27)(react@19.2.4)(typescript@5.7.3): dependencies: '@formatjs/ecma402-abstract': 3.0.7 '@formatjs/icu-messageformat-parser': 3.2.1 @@ -12307,7 +12513,7 @@ snapshots: '@types/react': 18.3.27 hoist-non-react-statics: 3.3.2 intl-messageformat: 11.0.8 - react: 19.2.3 + react: 19.2.4 tslib: 2.8.1 optionalDependencies: typescript: 5.7.3 @@ -12316,67 +12522,67 @@ snapshots: react-property@2.0.2: {} - react-redux@9.2.0(@types/react@18.3.27)(react@19.2.3)(redux@5.0.1): + react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 - react: 19.2.3 - use-sync-external-store: 1.5.0(react@19.2.3) + react: 19.2.4 + use-sync-external-store: 1.5.0(react@19.2.4) optionalDependencies: '@types/react': 18.3.27 redux: 5.0.1 react-refresh@0.18.0: {} - react-sparklines@1.7.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-sparklines@1.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: prop-types: 15.8.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) - react-sticky-box@2.0.5(react@19.2.3): + react-sticky-box@2.0.5(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 react-swipeable-views-core@0.14.0: dependencies: '@babel/runtime': 7.0.0 warning: 4.0.3 - react-swipeable-views-utils@0.14.0(react@19.2.3): + react-swipeable-views-utils@0.14.0(react@19.2.4): dependencies: '@babel/runtime': 7.0.0 keycode: 2.2.1 prop-types: 15.8.1 - react-event-listener: 0.6.6(react@19.2.3) + react-event-listener: 0.6.6(react@19.2.4) react-swipeable-views-core: 0.14.0 shallow-equal: 1.2.1 transitivePeerDependencies: - react - react-swipeable-views@0.14.0(react@19.2.3): + react-swipeable-views@0.14.0(react@19.2.4): dependencies: '@babel/runtime': 7.0.0 prop-types: 15.8.1 - react: 19.2.3 + react: 19.2.4 react-swipeable-views-core: 0.14.0 - react-swipeable-views-utils: 0.14.0(react@19.2.3) + react-swipeable-views-utils: 0.14.0(react@19.2.4) warning: 4.0.3 - react-virtuoso@4.18.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + react-virtuoso@4.18.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) react@18.3.1: dependencies: loose-envify: 1.4.0 - react@19.2.3: {} + react@19.2.4: {} - reactcss@1.2.3(react@19.2.3): + reactcss@1.2.3(react@19.2.4): dependencies: lodash: 4.17.23 - react: 19.2.3 + react: 19.2.4 read-cache@1.0.0: dependencies: @@ -13351,19 +13557,19 @@ snapshots: dependencies: punycode: 2.3.1 - use-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.3): + use-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4): dependencies: '@types/react': 18.3.27 mutative: 1.3.0 - react: 19.2.3 + react: 19.2.4 - use-sync-external-store@1.5.0(react@19.2.3): + use-sync-external-store@1.5.0(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 - use-sync-external-store@1.6.0(react@19.2.3): + use-sync-external-store@1.6.0(react@19.2.4): dependencies: - react: 19.2.3 + react: 19.2.4 util-deprecate@1.0.2: {} @@ -13385,7 +13591,7 @@ snapshots: varint@6.0.0: {} - vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.12.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@babel/code-frame': 7.29.0 chokidar: 4.0.3 @@ -13400,7 +13606,7 @@ snapshots: eslint: 8.57.1 meow: 13.2.0 optionator: 0.9.4 - oxlint: 1.47.0(oxlint-tsgolint@0.12.2) + oxlint: 1.47.0(oxlint-tsgolint@0.14.2) stylelint: 16.23.0(typescript@5.7.3) typescript: 5.7.3 @@ -13849,16 +14055,16 @@ snapshots: yocto-queue@0.1.0: {} - zustand-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.3)(zustand@5.0.9(@types/react@18.3.27)(immer@10.1.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3))): + zustand-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))): dependencies: '@types/react': 18.3.27 mutative: 1.3.0 - react: 19.2.3 - zustand: 5.0.9(@types/react@18.3.27)(immer@10.1.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) + react: 19.2.4 + zustand: 5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - zustand@5.0.9(@types/react@18.3.27)(immer@10.1.1)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)): + zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 18.3.27 immer: 10.1.1 - react: 19.2.3 - use-sync-external-store: 1.6.0(react@19.2.3) + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) From 31620881be0130ef498e7a32dc50ee8f7a5c39d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:02:10 +0100 Subject: [PATCH 045/264] nicolium: update lexical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/package.json | 22 +-- pnpm-lock.yaml | 344 ++++++++++++++++++------------------ 2 files changed, 182 insertions(+), 184 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index d1576ee3f..a3d4b3038 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -38,15 +38,15 @@ "@fontsource/noto-sans-javanese": "^5.2.8", "@fontsource/roboto-mono": "^5.2.8", "@fontsource/tajawal": "^5.2.7", - "@lexical/code": "^0.39.0", - "@lexical/hashtag": "^0.39.0", - "@lexical/link": "^0.39.0", - "@lexical/list": "^0.39.0", - "@lexical/markdown": "^0.39.0", - "@lexical/react": "^0.39.0", - "@lexical/rich-text": "^0.39.0", - "@lexical/selection": "^0.39.0", - "@lexical/utils": "^0.39.0", + "@lexical/code": "^0.40.0", + "@lexical/hashtag": "^0.40.0", + "@lexical/link": "^0.40.0", + "@lexical/list": "^0.40.0", + "@lexical/markdown": "^0.40.0", + "@lexical/react": "^0.40.0", + "@lexical/rich-text": "^0.40.0", + "@lexical/selection": "^0.40.0", + "@lexical/utils": "^0.40.0", "@mkljczk/url-purify": "^0.0.5", "@phosphor-icons/core": "^2.1.1", "@reach/combobox": "^0.18.0", @@ -92,7 +92,7 @@ "intl-pluralrules": "^2.0.1", "isomorphic-dompurify": "^2.35.0", "leaflet": "^1.9.4", - "lexical": "^0.39.0", + "lexical": "^0.40.0", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash-es": "^4.17.23", @@ -137,7 +137,7 @@ "@sentry/types": "^8.47.0", "@types/dom-chromium-ai": "^0.0.11", "@types/leaflet": "^1.9.21", - "@types/lodash": "^4.17.14", + "@types/lodash": "^4.17.24", "@types/path-browserify": "^1.0.3", "@types/react": "^19.2.7", "@types/react-color": "^3.0.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6964e7e1c..4b7c58d52 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,32 +116,32 @@ importers: specifier: ^5.2.7 version: 5.2.7 '@lexical/code': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/hashtag': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/link': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/list': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/markdown': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/react': - specifier: ^0.39.0 - version: 0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27) + specifier: ^0.40.0 + version: 0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27) '@lexical/rich-text': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/selection': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@lexical/utils': - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 '@mkljczk/url-purify': specifier: ^0.0.5 version: 0.0.5 @@ -278,8 +278,8 @@ importers: specifier: ^1.9.4 version: 1.9.4 lexical: - specifier: ^0.39.0 - version: 0.39.0 + specifier: ^0.40.0 + version: 0.40.0 line-awesome: specifier: ^1.3.0 version: 1.3.0 @@ -1729,77 +1729,77 @@ packages: '@keyv/serialize@1.1.0': resolution: {integrity: sha512-RlDgexML7Z63Q8BSaqhXdCYNBy/JQnqYIwxofUrNLGCblOMHp+xux2Q8nLMLlPpgHQPoU0Do8Z6btCpRBEqZ8g==} - '@lexical/clipboard@0.39.0': - resolution: {integrity: sha512-ylrHy8M+I5EH4utwqivslugqQhvgLTz9VEJdrb2RjbhKQEXwMcqKCRWh6cRfkYx64onE2YQE0nRIdzHhExEpLQ==} + '@lexical/clipboard@0.40.0': + resolution: {integrity: sha512-FWyAKwbGbmwLbG6biyxB/MQkELzcbd6E89Xufarbx/1VZ2pX/BMaeVD4J7ojHgIZ4omTNI6nKH26K4wWySCIGQ==} - '@lexical/code@0.39.0': - resolution: {integrity: sha512-3tqFOOzP5Z9nRkZPHZYmIyLXd28gMMrlAD3k2zxiH5vGnAqiYTezR24CpRDw1BaF2c8vCgY/9CNobZzjXivpIA==} + '@lexical/code@0.40.0': + resolution: {integrity: sha512-xbo3lW3OC7sz0UnoME0tXQcgnekmuvsGAb1HZpFHAF0ZUCquB6/yK0+9QzknrTBH9y3Urqx0vM54xAqNQISOtg==} - '@lexical/devtools-core@0.39.0': - resolution: {integrity: sha512-2ET2nFeRhcc2YMrn184wxoEOTLl3UOlugi8ozuZFa6F4UDMXPq7nZRhiQNgYzhE6Z7NLMFrcmghvx652JbEowg==} + '@lexical/devtools-core@0.40.0': + resolution: {integrity: sha512-qOlN4CYXHkdVxAzrZ1Cdu7bAYf1se+R3y7MfjWa6WFFukf7n6+RAoNIWTzpTLM7h7fOrsJLyP0Goi5IZI9wAOQ==} peerDependencies: react: '>=17.x' react-dom: '>=17.x' - '@lexical/dragon@0.39.0': - resolution: {integrity: sha512-JkcBAYPZGzfs29gtkePeJG9US1uwKW6PkUt8G4QZkMTt4QMDnadqXauFE+30rbpvRdeNcR7s+/jOuRHd5SurDQ==} + '@lexical/dragon@0.40.0': + resolution: {integrity: sha512-QWBZw89CAkw2b3Fl942DxJ7M8/XxFFvVubw9Z7Ac6wgUkGgtF2wrK9F0H8cPZ3pzbUqL1v1EtwW1/XhlLfmQqw==} - '@lexical/extension@0.39.0': - resolution: {integrity: sha512-mp/WcF8E53FWPiUHgHQz382J7u7C4+cELYNkC00dKaymf8NhS6M65Y8tyDikNGNUcLXSzaluwK0HkiKjTYGhVQ==} + '@lexical/extension@0.40.0': + resolution: {integrity: sha512-kipdm0f+xe8ctxHt9S3NPZazMX3ILqIk5xMWxX2svdsRc7qIeMkl+5SLWnBqQA+e5ztLJqa7GSA4WMqu/dBZDQ==} - '@lexical/hashtag@0.39.0': - resolution: {integrity: sha512-CFLNB74a607nC2GGcjKNPbo/ZnehnR3zz9+S5bfUg5dblSGKdCfxHiyr2cDwHY3dfOTu+qtimfh2Zqxz4dfghA==} + '@lexical/hashtag@0.40.0': + resolution: {integrity: sha512-k4PKWa8xyMHnnh+ewUboeFr8wKemk1GoMZ6LKN4qnqNKGCHANyJKeHRVUzyLjbiSwcXTrBqtUTV4ZrJXGRIkEw==} - '@lexical/history@0.39.0': - resolution: {integrity: sha512-kuctleDime0tRDxQNDW8i5d6D/ys5Npp2yoCBmdKS8HfS/jz7uPumfZcX7wvUvNAEVExh+bY9IxqIexyGkNUtA==} + '@lexical/history@0.40.0': + resolution: {integrity: sha512-Jo/9Z1fPlv+IpBkUaVstyKminXWjM1A1yR9UPZv4a3B3e8Rn0gqa+EaY5CXSvscnmh2EHSD8I4s59D4rirUy8Q==} - '@lexical/html@0.39.0': - resolution: {integrity: sha512-7VLWP5DpzBg3kKctpNK6PbhymKAtU6NAnKieopCfCIWlMW+EqpldteiIXGqSqrMRK0JWTmF1gKgr9nnQyOOsXw==} + '@lexical/html@0.40.0': + resolution: {integrity: sha512-5EZeHbp9Q3Op2KRoVfFvX4QyMizYW5SJkrWkGG6h6g/Z9EDNjb3C7Wjqx7ZosCHcFze6Pgic/0yEaCc2fSUcEg==} - '@lexical/link@0.39.0': - resolution: {integrity: sha512-L1jSF2BVRHDqIQbKYFcQt3CqtVIphRA3QAW2VooYPNlKeaAb/yfFS+C60GX1cj96b0rMlHKrNC17ik2aEBZKLQ==} + '@lexical/link@0.40.0': + resolution: {integrity: sha512-4EVMJQ6tKJR+1+YAJ2mhVsRR6Edk6b81hWKjnMZITKhEOKjylxO7bwuXYVYPEckBzL2mXkEhYX5aFnig5+HkMQ==} - '@lexical/list@0.39.0': - resolution: {integrity: sha512-mxgSxUrakTCHtC+gF30BChQBJTsCMiMgfC2H5VvhcFwXMgsKE/aK9+a+C/sSvvzCmPXqzYsuAcGkJcrY3e5xlw==} + '@lexical/list@0.40.0': + resolution: {integrity: sha512-YHStiN56DOc6tDu/3LwalPtHH8/1R+peiDF+/ePpla9aSDJclAqhxRBYsqSZhO6Hu5QhAEIi2Me7Gi+uZdmCTw==} - '@lexical/mark@0.39.0': - resolution: {integrity: sha512-wVs5498dWYOQ07FAHaFW6oYgNG3moBargf6es7+gHPzjlaoZ6Hd8sbvJtlT8F2RRlw+U+kUh4s8SjFuMSEJp0w==} + '@lexical/mark@0.40.0': + resolution: {integrity: sha512-9XJ0PQmeq5tDSgathAQs1ePMM5zaCBCbD4ShMXJf3fW1udI7e/Swn0Loxw9/VnER9fnVkyejGMC2oMWDQ80FVw==} - '@lexical/markdown@0.39.0': - resolution: {integrity: sha512-mPaKH2FSwRwU2bDbMiMtdOridaEvSLU3Q5l7bqYE+TW799C/1EEtiv4xSkI01SjV9YOxNf24VNOipAMymPueKA==} + '@lexical/markdown@0.40.0': + resolution: {integrity: sha512-J0vO4jSPZaazBgFafJhLYaJPBxSMk1nhGnNiu6+TojqOe3tx/0vukCafowNxBDruZFns+r7HsSs+vkmGJtGsrA==} - '@lexical/offset@0.39.0': - resolution: {integrity: sha512-8p+16AgFsG8ecZVQlFO6TQ+zHHHg7LKPNdm9BkklkJux41Y1+9rlPO12Mgbi4x2Hy2pRA8Gd/Su3hySGqEEVlA==} + '@lexical/offset@0.40.0': + resolution: {integrity: sha512-USytxiqB/mU6tKy2nXs4jhgCES90l8N5lCYxVbho2L/cVXAzBCp1epG///B6Vgm+twSj+jQjhGZioktILnG+FQ==} - '@lexical/overflow@0.39.0': - resolution: {integrity: sha512-BLtF4MNDrTNQFgryw6MPWh2Fj4GMjqC/6p9bbnZ9fdwMWKGSbsSNcK9PLlBwg3IzEK3XiibFDHUbsETwUd/bfw==} + '@lexical/overflow@0.40.0': + resolution: {integrity: sha512-T1LE8R7LloV9t8m+5IQ7Djkqcbd4mOoX85Jh8cKQ58TFHbkwi8nSe+FJUi3fucOI2atDq9QZCJnpRUCHw4eEwQ==} - '@lexical/plain-text@0.39.0': - resolution: {integrity: sha512-Ep0PGF7GlBNgiJJh/DBEPLt1WXyHUb7bCYZ4MUbD31AiJdG0p5a/g9dVTUr4QtNlCIXBCZjuatHyp6e2mzMacg==} + '@lexical/plain-text@0.40.0': + resolution: {integrity: sha512-mr6J1Fu34MwUNOPkzn3l/fZBpD91HLAxs6RBAQ6mfSW7jLXBVDlvhiB4ez9ud/jZ0bgLaY8EG7ooT7htdlkBUQ==} - '@lexical/react@0.39.0': - resolution: {integrity: sha512-6ySVb5xv99GIkVzio4qqOBxkPgOSSeFAB4o9bVqtg72JbCoEKZPnWq5VVurGe1uiRJM8jvqTseM9mo2zTvUfXQ==} + '@lexical/react@0.40.0': + resolution: {integrity: sha512-+J73I21LNT659f1IMTbxe055mKnt4H2SkHp3UDxrmWmkWmDaPcq7XG07i8/xCPtYz15aEXUQycqaWQc5pAqF5g==} peerDependencies: react: '>=17.x' react-dom: '>=17.x' - '@lexical/rich-text@0.39.0': - resolution: {integrity: sha512-UoSgRi09nLP/mmD3ijdZycr9icnqlb761rzHC1gicuPDdTu0ruxAFbGanSE2h36ihSu0IUHwkpf4gBpgPPqWBw==} + '@lexical/rich-text@0.40.0': + resolution: {integrity: sha512-aHW9gSYGzEfZNxx14j2xJhihrnKaWA1aoudteP4r7TkNlbfJ3xG9dxp7ItwrAKJlfvI0gkMc1/aZOUIqESbAkw==} - '@lexical/selection@0.39.0': - resolution: {integrity: sha512-j0cgNuTKDCdf/4MzRnAUwEqG6C/WQp18k2WKmX5KIVZJlhnGIJmlgSBrxjo8AuZ16DIHxTm2XNB4cUDCgZNuPA==} + '@lexical/selection@0.40.0': + resolution: {integrity: sha512-iFwZufMlIx9fZ+K3NQip9oxoHzuP+V9rVdkLnfUWC7aO4HNxVPSryEfUnbAs+F5xlOzyVHsu7Xa+CMHfIL8/gQ==} - '@lexical/table@0.39.0': - resolution: {integrity: sha512-1eH11kV4bJ0fufCYl8DpE19kHwqUI8Ev5CZwivfAtC3ntwyNkeEpjCc0pqeYYIWN/4rTZ5jgB3IJV4FntyfCzw==} + '@lexical/table@0.40.0': + resolution: {integrity: sha512-pn5T7Uc80dH8LR2d/sepKc8SjiKKir40AF+5hW0MJAOYfoizd10x72AxPcM6iBYvI0rdW7rD7Lhko1qLmZ0pOw==} - '@lexical/text@0.39.0': - resolution: {integrity: sha512-fcIgejtIgfMAkxio6BO1eLA2eb4oRIFoUVA2jAXdCaLVHrG/cizitbygPrgWnWd8nt1WlMuS4lxa0PJl7h7Lqg==} + '@lexical/text@0.40.0': + resolution: {integrity: sha512-cTMBrHPzlIRQUkopIUhPwSzcqQDGsCEdjpStylJficwMav9vG+/sJNSG4PFO+4ss5BZ/x7AoM3KH/P2SOzqdbw==} - '@lexical/utils@0.39.0': - resolution: {integrity: sha512-8YChidpMJpwQc4nex29FKUeuZzC++QCS/Jt46lPuy1GS/BZQoPHFKQ5hyVvM9QVhc5CEs4WGNoaCZvZIVN8bQw==} + '@lexical/utils@0.40.0': + resolution: {integrity: sha512-3wkzgQxeb137GtaGWZI23XYB+omGjfYlrvAPJOqcb5z8yS7iAiuHwWULdmi1/jPBClS7z9N8pkNzq06BP8QlZA==} - '@lexical/yjs@0.39.0': - resolution: {integrity: sha512-peBrzIDoRWeyX9XTilKVdeJua6A+RZ24CG7lgGLEhmNSGCqpj9FqlC1Wtrul4wTSh85KlDeI1Nq30gnyeNKWYA==} + '@lexical/yjs@0.40.0': + resolution: {integrity: sha512-wpasbrlfzBnHhyuUunxZcGwOH+bqfxUuavGgwxDlTTlHJMnyF5bUG7ADcuqd3gFLB+dpABL0lWo6VxdSOo7fdg==} peerDependencies: yjs: '>=13.5.22' @@ -4856,8 +4856,8 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lexical@0.39.0: - resolution: {integrity: sha512-lpLv7MEJH5QDujEDlYqettL3ATVtNYjqyimzqgrm0RvCm3AO9WXSdsgTxuN7IAZRu88xkxCDeYubeUf4mNZVdg==} + lexical@0.40.0: + resolution: {integrity: sha512-wNvd/AY13h/QJYvx565M/FSdRjy0l99W5/MFA2x+mbK3KnKa5BifZbHZ1J4/YssCkdyZhSlLwFkyDaYi7l2Dsw==} lib0@0.2.117: resolution: {integrity: sha512-DeXj9X5xDCjgKLU/7RR+/HQEVzuuEUiwldwOGsHK/sfAfELGWEyTcf0x+uOvCvK3O2zPmZePXWL85vtia6GyZw==} @@ -8334,165 +8334,163 @@ snapshots: '@keyv/serialize@1.1.0': {} - '@lexical/clipboard@0.39.0': + '@lexical/clipboard@0.40.0': dependencies: - '@lexical/html': 0.39.0 - '@lexical/list': 0.39.0 - '@lexical/selection': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/html': 0.40.0 + '@lexical/list': 0.40.0 + '@lexical/selection': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/code@0.39.0': + '@lexical/code@0.40.0': dependencies: - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 prismjs: 1.30.0 - '@lexical/devtools-core@0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@lexical/devtools-core@0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@lexical/html': 0.39.0 - '@lexical/link': 0.39.0 - '@lexical/mark': 0.39.0 - '@lexical/table': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/html': 0.40.0 + '@lexical/link': 0.40.0 + '@lexical/mark': 0.40.0 + '@lexical/table': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@lexical/dragon@0.39.0': + '@lexical/dragon@0.40.0': dependencies: - '@lexical/extension': 0.39.0 - lexical: 0.39.0 + '@lexical/extension': 0.40.0 + lexical: 0.40.0 - '@lexical/extension@0.39.0': + '@lexical/extension@0.40.0': dependencies: - '@lexical/utils': 0.39.0 + '@lexical/utils': 0.40.0 '@preact/signals-core': 1.12.1 - lexical: 0.39.0 + lexical: 0.40.0 - '@lexical/hashtag@0.39.0': + '@lexical/hashtag@0.40.0': dependencies: - '@lexical/text': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/text': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/history@0.39.0': + '@lexical/history@0.40.0': dependencies: - '@lexical/extension': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/extension': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/html@0.39.0': + '@lexical/html@0.40.0': dependencies: - '@lexical/selection': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/selection': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/link@0.39.0': + '@lexical/link@0.40.0': dependencies: - '@lexical/extension': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/extension': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/list@0.39.0': + '@lexical/list@0.40.0': dependencies: - '@lexical/extension': 0.39.0 - '@lexical/selection': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/extension': 0.40.0 + '@lexical/selection': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/mark@0.39.0': + '@lexical/mark@0.40.0': dependencies: - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/markdown@0.39.0': + '@lexical/markdown@0.40.0': dependencies: - '@lexical/code': 0.39.0 - '@lexical/link': 0.39.0 - '@lexical/list': 0.39.0 - '@lexical/rich-text': 0.39.0 - '@lexical/text': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/code': 0.40.0 + '@lexical/link': 0.40.0 + '@lexical/list': 0.40.0 + '@lexical/rich-text': 0.40.0 + '@lexical/text': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/offset@0.39.0': + '@lexical/offset@0.40.0': dependencies: - lexical: 0.39.0 + lexical: 0.40.0 - '@lexical/overflow@0.39.0': + '@lexical/overflow@0.40.0': dependencies: - lexical: 0.39.0 + lexical: 0.40.0 - '@lexical/plain-text@0.39.0': + '@lexical/plain-text@0.40.0': dependencies: - '@lexical/clipboard': 0.39.0 - '@lexical/dragon': 0.39.0 - '@lexical/selection': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/clipboard': 0.40.0 + '@lexical/dragon': 0.40.0 + '@lexical/selection': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/react@0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27)': + '@lexical/react@0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27)': dependencies: '@floating-ui/react': 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@lexical/devtools-core': 0.39.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@lexical/dragon': 0.39.0 - '@lexical/extension': 0.39.0 - '@lexical/hashtag': 0.39.0 - '@lexical/history': 0.39.0 - '@lexical/link': 0.39.0 - '@lexical/list': 0.39.0 - '@lexical/mark': 0.39.0 - '@lexical/markdown': 0.39.0 - '@lexical/overflow': 0.39.0 - '@lexical/plain-text': 0.39.0 - '@lexical/rich-text': 0.39.0 - '@lexical/table': 0.39.0 - '@lexical/text': 0.39.0 - '@lexical/utils': 0.39.0 - '@lexical/yjs': 0.39.0(yjs@13.6.27) - lexical: 0.39.0 + '@lexical/devtools-core': 0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@lexical/dragon': 0.40.0 + '@lexical/extension': 0.40.0 + '@lexical/hashtag': 0.40.0 + '@lexical/history': 0.40.0 + '@lexical/link': 0.40.0 + '@lexical/list': 0.40.0 + '@lexical/mark': 0.40.0 + '@lexical/markdown': 0.40.0 + '@lexical/overflow': 0.40.0 + '@lexical/plain-text': 0.40.0 + '@lexical/rich-text': 0.40.0 + '@lexical/table': 0.40.0 + '@lexical/text': 0.40.0 + '@lexical/utils': 0.40.0 + '@lexical/yjs': 0.40.0(yjs@13.6.27) + lexical: 0.40.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-error-boundary: 6.0.0(react@19.2.4) transitivePeerDependencies: - yjs - '@lexical/rich-text@0.39.0': + '@lexical/rich-text@0.40.0': dependencies: - '@lexical/clipboard': 0.39.0 - '@lexical/dragon': 0.39.0 - '@lexical/selection': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/clipboard': 0.40.0 + '@lexical/dragon': 0.40.0 + '@lexical/selection': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/selection@0.39.0': + '@lexical/selection@0.40.0': dependencies: - lexical: 0.39.0 + lexical: 0.40.0 - '@lexical/table@0.39.0': + '@lexical/table@0.40.0': dependencies: - '@lexical/clipboard': 0.39.0 - '@lexical/extension': 0.39.0 - '@lexical/utils': 0.39.0 - lexical: 0.39.0 + '@lexical/clipboard': 0.40.0 + '@lexical/extension': 0.40.0 + '@lexical/utils': 0.40.0 + lexical: 0.40.0 - '@lexical/text@0.39.0': + '@lexical/text@0.40.0': dependencies: - lexical: 0.39.0 + lexical: 0.40.0 - '@lexical/utils@0.39.0': + '@lexical/utils@0.40.0': dependencies: - '@lexical/list': 0.39.0 - '@lexical/selection': 0.39.0 - '@lexical/table': 0.39.0 - lexical: 0.39.0 + '@lexical/selection': 0.40.0 + lexical: 0.40.0 - '@lexical/yjs@0.39.0(yjs@13.6.27)': + '@lexical/yjs@0.40.0(yjs@13.6.27)': dependencies: - '@lexical/offset': 0.39.0 - '@lexical/selection': 0.39.0 - lexical: 0.39.0 + '@lexical/offset': 0.40.0 + '@lexical/selection': 0.40.0 + lexical: 0.40.0 yjs: 13.6.27 '@material/material-color-utilities@0.3.0': {} @@ -11633,7 +11631,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lexical@0.39.0: {} + lexical@0.40.0: {} lib0@0.2.117: dependencies: From df61b29ea23a3a4fbcfcc5a7deb843314a3c620b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:07:40 +0100 Subject: [PATCH 046/264] nicolium: update some more packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/package.json | 37 +- pnpm-lock.yaml | 1372 ++++++++++++++++++----------------- 2 files changed, 718 insertions(+), 691 deletions(-) diff --git a/packages/pl-fe/package.json b/packages/pl-fe/package.json index a3d4b3038..ccb452edc 100644 --- a/packages/pl-fe/package.json +++ b/packages/pl-fe/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@emoji-mart/data": "^1.2.1", - "@floating-ui/react": "^0.27.16", + "@floating-ui/react": "^0.27.18", "@fontsource/inter": "^5.2.8", "@fontsource/noto-sans-javanese": "^5.2.8", "@fontsource/roboto-mono": "^5.2.8", @@ -59,22 +59,22 @@ "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/forms": "^0.5.10", "@tailwindcss/typography": "^0.5.16", - "@tanstack/react-pacer": "^0.16.4", + "@tanstack/react-pacer": "^0.20.0", "@tanstack/react-query": "^5.90.21", "@tanstack/react-router": "^1.162.8", - "@transfem-org/sfm-js": "^0.24.6", + "@transfem-org/sfm-js": "^0.26.1", "@twemoji/svg": "^15.0.0", "@uidotdev/usehooks": "^2.4.1", "@use-gesture/react": "^10.3.1", - "@yornaath/batshit": "^0.11.2", + "@yornaath/batshit": "^0.14.0", "abortcontroller-polyfill": "^1.7.8", "autoprefixer": "^10.4.24", "blurhash": "^2.0.5", - "bowser": "^2.13.1", + "bowser": "^2.14.1", "browserslist": "^4.28.1", "browserslist-to-esbuild": "^2.1.1", "clsx": "^2.1.1", - "core-js": "^3.39.0", + "core-js": "^3.48.0", "cryptocurrency-icons": "^0.18.1", "cssnano": "^6.0.0", "detect-passive-events": "^2.0.0", @@ -88,15 +88,15 @@ "graphemesplit": "^2.4.4", "html-react-parser": "^5.2.17", "intersection-observer": "^0.12.2", - "intl-messageformat": "^10.7.18", + "intl-messageformat": "^11.1.2", "intl-pluralrules": "^2.0.1", - "isomorphic-dompurify": "^2.35.0", + "isomorphic-dompurify": "^3.0.0", "leaflet": "^1.9.4", "lexical": "^0.40.0", "line-awesome": "^1.3.0", "localforage": "^1.10.0", "lodash-es": "^4.17.23", - "mini-css-extract-plugin": "^2.9.4", + "mini-css-extract-plugin": "^2.10.0", "mutative": "^1.3.0", "object-to-formdata": "^4.5.1", "path-browserify": "^1.0.1", @@ -113,7 +113,7 @@ "react-helmet-async": "^2.0.5", "react-hot-toast": "^2.6.0", "react-inlinesvg": "^4.1.8", - "react-intl": "^8.0.10", + "react-intl": "^8.1.3", "react-redux": "^9.0.4", "react-sparklines": "^1.7.0", "react-sticky-box": "^2.0.5", @@ -123,7 +123,7 @@ "redux-thunk": "^3.1.0", "reselect": "^5.1.1", "resize-observer": "^1.0.4", - "sass-embedded": "^1.93.3", + "sass-embedded": "^1.97.3", "stringz": "^2.1.0", "tabbable": "^6.4.0", "use-mutative": "^1.3.1", @@ -135,29 +135,28 @@ "devDependencies": { "@formatjs/cli": "^6.13.0", "@sentry/types": "^8.47.0", - "@types/dom-chromium-ai": "^0.0.11", + "@types/dom-chromium-ai": "^0.0.14", "@types/leaflet": "^1.9.21", "@types/lodash": "^4.17.24", "@types/path-browserify": "^1.0.3", - "@types/react": "^19.2.7", + "@types/react": "^19.2.14", "@types/react-color": "^3.0.13", "@types/react-dom": "^19.2.3", - "@types/react-router-dom": "^5.3.3", "@types/react-sparklines": "^1.7.5", "@types/react-swipeable-views": "^0.13.6", "@vitejs/plugin-react": "^5.1.3", - "eslint-plugin-formatjs": "^5.4.2", - "globals": "^15.14.0", + "eslint-plugin-formatjs": "^6.2.0", + "globals": "^17.3.0", "oxfmt": "^0.35.0", - "oxlint": "^1.47.0", + "oxlint": "^1.50.0", "oxlint-tsgolint": "^0.14.2", "rollup-plugin-bundle-stats": "^4.21.10", "stylelint": "^16.12.0", "stylelint-config-standard-scss": "^12.0.0", - "tailwindcss": "^3.4.17", + "tailwindcss": "^3.4.19", "tslib": "^2.8.1", "type-fest": "^4.30.1", - "typescript": "5.7.3", + "typescript": "5.9.3", "vite": "^7.3.1", "vite-plugin-checker": "^0.12.0", "vite-plugin-compile-time": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b7c58d52..707f591ef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,10 +87,10 @@ importers: version: 5.9.2 vite: specifier: ^7.0.0 - version: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + version: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) ws: specifier: ^8.18.3 version: 8.18.3 @@ -101,8 +101,8 @@ importers: specifier: ^1.2.1 version: 1.2.1 '@floating-ui/react': - specifier: ^0.27.16 - version: 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^0.27.18 + version: 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@fontsource/inter': specifier: ^5.2.8 version: 5.2.8 @@ -171,16 +171,16 @@ importers: version: 8.55.0(react@19.2.4) '@tailwindcss/aspect-ratio': specifier: ^0.4.2 - version: 0.4.2(tailwindcss@3.4.17) + version: 0.4.2(tailwindcss@3.4.19) '@tailwindcss/forms': specifier: ^0.5.10 - version: 0.5.10(tailwindcss@3.4.17) + version: 0.5.10(tailwindcss@3.4.19) '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.16(tailwindcss@3.4.17) + version: 0.5.16(tailwindcss@3.4.19) '@tanstack/react-pacer': - specifier: ^0.16.4 - version: 0.16.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^0.20.0 + version: 0.20.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@tanstack/react-query': specifier: ^5.90.21 version: 5.90.21(react@19.2.4) @@ -188,8 +188,8 @@ importers: specifier: ^1.162.8 version: 1.162.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@transfem-org/sfm-js': - specifier: ^0.24.6 - version: 0.24.8 + specifier: ^0.26.1 + version: 0.26.1 '@twemoji/svg': specifier: ^15.0.0 version: 15.0.0 @@ -200,8 +200,8 @@ importers: specifier: ^10.3.1 version: 10.3.1(react@19.2.4) '@yornaath/batshit': - specifier: ^0.11.2 - version: 0.11.2 + specifier: ^0.14.0 + version: 0.14.0 abortcontroller-polyfill: specifier: ^1.7.8 version: 1.7.8 @@ -212,8 +212,8 @@ importers: specifier: ^2.0.5 version: 2.0.5 bowser: - specifier: ^2.13.1 - version: 2.13.1 + specifier: ^2.14.1 + version: 2.14.1 browserslist: specifier: ^4.28.1 version: 4.28.1 @@ -224,8 +224,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 core-js: - specifier: ^3.39.0 - version: 3.44.0 + specifier: ^3.48.0 + version: 3.48.0 cryptocurrency-icons: specifier: ^0.18.1 version: 0.18.1 @@ -266,14 +266,14 @@ importers: specifier: ^0.12.2 version: 0.12.2 intl-messageformat: - specifier: ^10.7.18 - version: 10.7.18 + specifier: ^11.1.2 + version: 11.1.2 intl-pluralrules: specifier: ^2.0.1 version: 2.0.1 isomorphic-dompurify: - specifier: ^2.35.0 - version: 2.35.0 + specifier: ^3.0.0 + version: 3.0.0 leaflet: specifier: ^1.9.4 version: 1.9.4 @@ -290,8 +290,8 @@ importers: specifier: ^4.17.23 version: 4.17.23 mini-css-extract-plugin: - specifier: ^2.9.4 - version: 2.9.4(webpack@5.101.0(esbuild@0.24.2)) + specifier: ^2.10.0 + version: 2.10.0(webpack@5.101.0(esbuild@0.24.2)) mutative: specifier: ^1.3.0 version: 1.3.0 @@ -341,8 +341,8 @@ importers: specifier: ^4.1.8 version: 4.2.0(react@19.2.4) react-intl: - specifier: ^8.0.10 - version: 8.0.10(@types/react@18.3.27)(react@19.2.4)(typescript@5.7.3) + specifier: ^8.1.3 + version: 8.1.3(@types/react@18.3.27)(react@19.2.4)(typescript@5.9.3) react-redux: specifier: ^9.0.4 version: 9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1) @@ -371,8 +371,8 @@ importers: specifier: ^1.0.4 version: 1.0.4 sass-embedded: - specifier: ^1.93.3 - version: 1.93.3 + specifier: ^1.97.3 + version: 1.97.3 stringz: specifier: ^2.1.0 version: 2.1.0 @@ -387,7 +387,7 @@ importers: version: 0.12.5 valibot: specifier: ^1.2.0 - version: 1.2.0(typescript@5.7.3) + version: 1.2.0(typescript@5.9.3) zustand: specifier: ^5.0.11 version: 5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) @@ -402,14 +402,14 @@ importers: specifier: ^8.47.0 version: 8.55.0 '@types/dom-chromium-ai': - specifier: ^0.0.11 - version: 0.0.11 + specifier: ^0.0.14 + version: 0.0.14 '@types/leaflet': specifier: ^1.9.21 version: 1.9.21 '@types/lodash': - specifier: ^4.17.14 - version: 4.17.20 + specifier: ^4.17.24 + version: 4.17.24 '@types/path-browserify': specifier: ^1.0.3 version: 1.0.3 @@ -422,9 +422,6 @@ importers: '@types/react-dom': specifier: ^18.3.5 version: 18.3.7(@types/react@18.3.27) - '@types/react-router-dom': - specifier: ^5.3.3 - version: 5.3.3 '@types/react-sparklines': specifier: ^1.7.5 version: 1.7.5 @@ -433,34 +430,34 @@ importers: version: 0.13.6 '@vitejs/plugin-react': specifier: ^5.1.3 - version: 5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) eslint-plugin-formatjs: - specifier: ^5.4.2 - version: 5.4.2(eslint@8.57.1)(typescript@5.7.3) + specifier: ^6.2.0 + version: 6.2.0(eslint@8.57.1) globals: - specifier: ^15.14.0 - version: 15.15.0 + specifier: ^17.3.0 + version: 17.3.0 oxfmt: specifier: ^0.35.0 version: 0.35.0 oxlint: - specifier: ^1.47.0 - version: 1.47.0(oxlint-tsgolint@0.14.2) + specifier: ^1.50.0 + version: 1.50.0(oxlint-tsgolint@0.14.2) oxlint-tsgolint: specifier: ^0.14.2 version: 0.14.2 rollup-plugin-bundle-stats: specifier: ^4.21.10 - version: 4.21.10(core-js@3.44.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) stylelint: specifier: ^16.12.0 - version: 16.23.0(typescript@5.7.3) + version: 16.23.0(typescript@5.9.3) stylelint-config-standard-scss: specifier: ^12.0.0 - version: 12.0.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.7.3)) + version: 12.0.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.9.3)) tailwindcss: - specifier: ^3.4.17 - version: 3.4.17 + specifier: ^3.4.19 + version: 3.4.19 tslib: specifier: ^2.8.1 version: 2.8.1 @@ -468,29 +465,29 @@ importers: specifier: ^4.30.1 version: 4.41.0 typescript: - specifier: 5.7.3 - version: 5.7.3 + specifier: 5.9.3 + version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + version: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) vite-plugin-checker: specifier: ^0.12.0 - version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) vite-plugin-compile-time: specifier: ^0.4.6 - version: 0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) vite-plugin-html: specifier: ^3.2.2 - version: 3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) vite-plugin-pwa: specifier: ^1.2.0 - version: 1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + version: 1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-require: specifier: ^1.2.14 - version: 1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) vite-plugin-static-copy: specifier: ^3.2.0 - version: 3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) packages/pl-hooks: dependencies: @@ -545,15 +542,15 @@ importers: version: 5.9.2 vite: specifier: ^8.0.0-beta.14 - version: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + version: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) vite-plugin-dts: specifier: ^4.2.1 - version: 4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) packages: - '@acemir/cssom@0.9.30': - resolution: {integrity: sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg==} + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} @@ -565,11 +562,12 @@ packages: peerDependencies: ajv: '>=8' - '@asamuzakjp/css-color@4.1.1': - resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==} + '@asamuzakjp/css-color@5.0.1': + resolution: {integrity: sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@asamuzakjp/dom-selector@6.7.6': - resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==} + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} '@asamuzakjp/nwsapi@2.3.9': resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} @@ -1126,6 +1124,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bufbuild/protobuf@2.10.1': resolution: {integrity: sha512-ckS3+vyJb5qGpEYv/s1OebUHDi/xSNtfgw1wqKZo7MR9F2z+qXr0q5XagafAG/9O0QPVIUfST0smluYSTpYFkg==} @@ -1153,23 +1155,23 @@ packages: core-js: ^3.0.0 lodash: ^4.0.0 - '@csstools/color-helpers@5.1.0': - resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} - engines: {node: '>=18'} + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} - '@csstools/css-calc@2.1.4': - resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} - engines: {node: '>=18'} + '@csstools/css-calc@3.1.1': + resolution: {integrity: sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 - '@csstools/css-color-parser@3.1.0': - resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} - engines: {node: '>=18'} + '@csstools/css-color-parser@4.0.2': + resolution: {integrity: sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==} + engines: {node: '>=20.19.0'} peerDependencies: - '@csstools/css-parser-algorithms': ^3.0.5 - '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 '@csstools/css-parser-algorithms@3.0.5': resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} @@ -1177,14 +1179,23 @@ packages: peerDependencies: '@csstools/css-tokenizer': ^3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.22': - resolution: {integrity: sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw==} - engines: {node: '>=18'} + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.28': + resolution: {integrity: sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==} '@csstools/css-tokenizer@3.0.4': resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@csstools/media-query-list-parser@4.0.3': resolution: {integrity: sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==} engines: {node: '>=18'} @@ -1546,29 +1557,29 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - '@exodus/bytes@1.8.0': - resolution: {integrity: sha512-8JPn18Bcp8Uo1T82gR8lh2guEOa5KKU/IEKvvdp0sgmi7coPBWf1Doi1EXsGZb2ehc8ym/StJCjffYV+ne7sXQ==} + '@exodus/bytes@1.14.1': + resolution: {integrity: sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@exodus/crypto': ^1.0.0-rc.4 + '@noble/hashes': ^1.8.0 || ^2.0.0 peerDependenciesMeta: - '@exodus/crypto': + '@noble/hashes': optional: true - '@floating-ui/core@1.7.3': - resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + '@floating-ui/core@1.7.4': + resolution: {integrity: sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==} - '@floating-ui/dom@1.7.4': - resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + '@floating-ui/dom@1.7.5': + resolution: {integrity: sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==} - '@floating-ui/react-dom@2.1.6': - resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + '@floating-ui/react-dom@2.1.7': + resolution: {integrity: sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' - '@floating-ui/react@0.27.16': - resolution: {integrity: sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==} + '@floating-ui/react@0.27.18': + resolution: {integrity: sha512-xJWJxvmy3a05j643gQt+pRbht5XnTlGpsEsAPnMi5F5YTOEEJymA90uZKBD8OvIv5XvZ1qi4GcccSlqT3Bq44Q==} peerDependencies: react: '>=17.0.0' react-dom: '>=17.0.0' @@ -1616,46 +1627,32 @@ packages: vue: optional: true - '@formatjs/ecma402-abstract@2.3.6': - resolution: {integrity: sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==} + '@formatjs/ecma402-abstract@3.1.1': + resolution: {integrity: sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==} - '@formatjs/ecma402-abstract@3.0.7': - resolution: {integrity: sha512-U55Yulf37vBXN0C7gHm7hrxULVrcrhpQBcdLmIN2rpYpLfC5eIpa1JRX9efjU74gfzjK/MSmSG3Lxv3E4ZNZIw==} + '@formatjs/fast-memoize@3.1.0': + resolution: {integrity: sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==} - '@formatjs/fast-memoize@2.2.7': - resolution: {integrity: sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==} + '@formatjs/icu-messageformat-parser@3.5.1': + resolution: {integrity: sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==} - '@formatjs/fast-memoize@3.0.2': - resolution: {integrity: sha512-YFApUDWFmjpPwAE7VcY7PYVjm6JaLZOAo0UfCQj1/OGi/1QtduG9kIBHmVC551M6AI01qvuP5kjbDebrZOT4Vg==} + '@formatjs/icu-skeleton-parser@2.1.1': + resolution: {integrity: sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==} - '@formatjs/icu-messageformat-parser@2.11.4': - resolution: {integrity: sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==} + '@formatjs/intl-localematcher@0.8.1': + resolution: {integrity: sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==} - '@formatjs/icu-messageformat-parser@3.2.1': - resolution: {integrity: sha512-DEECn8HEHtI4dvfKtTfvDOZ9nCTAJ2ESXGPRGKe4dkn/RE9w/G0NjgP/kFAQJbwIKWHo+BRxpee1bQKJ4lF6pg==} - - '@formatjs/icu-skeleton-parser@1.8.16': - resolution: {integrity: sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==} - - '@formatjs/icu-skeleton-parser@2.0.7': - resolution: {integrity: sha512-/LEeQ2gOU7ujm7LJk07OYYOpsOtIH/6ma78vTHvZNGZ6m0wn3gxQqU39HEpXZfez6aIhGh7Psde2H2ILj5wb0Q==} - - '@formatjs/intl-localematcher@0.6.2': - resolution: {integrity: sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==} - - '@formatjs/intl-localematcher@0.7.4': - resolution: {integrity: sha512-AWsSZupIBMU/y04Nj24CjohyNVyfItMJPxSzX5OJwedDEIbGLOHkPxCjAeLeiLF2dw4xmQA8psktdi9MaebBQw==} - - '@formatjs/intl@4.0.8': - resolution: {integrity: sha512-+tf/K6NPHe0X5e2xTl7zsIeBMv2/G1czennknmPbNDxBbeCrWKBSIQHv1Pd92Jnwv59BnXL1ajn18bParbGdpg==} + '@formatjs/intl@4.1.2': + resolution: {integrity: sha512-V60fNY/X/7zqmRffr7qPwscGmVGYDmlKF069mSQ2a/7fE22q602NtIfOQY8vzRA63Gr/O/U6vjRVBHMabrnA9A==} peerDependencies: typescript: ^5.6.0 peerDependenciesMeta: typescript: optional: true - '@formatjs/ts-transformer@3.14.2': - resolution: {integrity: sha512-c47ij+2Xi4jMDO3Hz01BDF3yB4575Gkoq24sFzVw1K1kpHvITsFfdlXQbhxScBwJi2gBhMpuZ++XsTUZ9O0Law==} + '@formatjs/ts-transformer@4.4.0': + resolution: {integrity: sha512-lFDp9Rbpxk5Dt8O1/I9VG5btqKbOkjT4snSa73HO1YTJ9KGeXPKA7aWVgHFXJVVq0KluhbZiCoPJVHC4ZREgxw==} + engines: {node: '>= 20.12.0'} peerDependencies: ts-jest: ^29 peerDependenciesMeta: @@ -2118,114 +2115,228 @@ packages: cpu: [arm] os: [android] + '@oxlint/binding-android-arm-eabi@1.50.0': + resolution: {integrity: sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxlint/binding-android-arm64@1.47.0': resolution: {integrity: sha512-xh02lsTF1TAkR+SZrRMYHR/xCx8Wg2MAHxJNdHVpAKELh9/yE9h4LJeqAOBbIb3YYn8o/D97U9VmkvkfJfrHfw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxlint/binding-android-arm64@1.50.0': + resolution: {integrity: sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxlint/binding-darwin-arm64@1.47.0': resolution: {integrity: sha512-OSOfNJqabOYbkyQDGT5pdoL+05qgyrmlQrvtCO58M4iKGEQ/xf3XkkKj7ws+hO+k8Y4VF4zGlBsJlwqy7qBcHA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxlint/binding-darwin-arm64@1.50.0': + resolution: {integrity: sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxlint/binding-darwin-x64@1.47.0': resolution: {integrity: sha512-hP2bOI4IWNS+F6pVXWtRshSTuJ1qCRZgDgVUg6EBUqsRy+ExkEPJkx+YmIuxgdCduYK1LKptLNFuQLJP8voPbQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxlint/binding-darwin-x64@1.50.0': + resolution: {integrity: sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxlint/binding-freebsd-x64@1.47.0': resolution: {integrity: sha512-F55jIEH5xmGu7S661Uho8vGiLFk0bY3A/g4J8CTKiLJnYu/PSMZ2WxFoy5Hji6qvFuujrrM9Q8XXbMO0fKOYPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxlint/binding-freebsd-x64@1.50.0': + resolution: {integrity: sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxlint/binding-linux-arm-gnueabihf@1.47.0': resolution: {integrity: sha512-wxmOn/wns/WKPXUC1fo5mu9pMZPVOu8hsynaVDrgmmXMdHKS7on6bA5cPauFFN9tJXNdsjW26AK9lpfu3IfHBQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + resolution: {integrity: sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxlint/binding-linux-arm-musleabihf@1.47.0': resolution: {integrity: sha512-KJTmVIA/GqRlM2K+ZROH30VMdydEU7bDTY35fNg3tOPzQRIs2deLZlY/9JWwdWo1F/9mIYmpbdCmPqtKhWNOPg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + resolution: {integrity: sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxlint/binding-linux-arm64-gnu@1.47.0': resolution: {integrity: sha512-PF7ELcFg1GVlS0X0ZB6aWiXobjLrAKer3T8YEkwIoO8RwWiAMkL3n3gbleg895BuZkHVlJ2kPRUwfrhHrVkD1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@oxlint/binding-linux-arm64-gnu@1.50.0': + resolution: {integrity: sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@oxlint/binding-linux-arm64-musl@1.47.0': resolution: {integrity: sha512-4BezLRO5cu0asf0Jp1gkrnn2OHiXrPPPEfBTxq1k5/yJ2zdGGTmZxHD2KF2voR23wb8Elyu3iQawXo7wvIZq0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + '@oxlint/binding-linux-arm64-musl@1.50.0': + resolution: {integrity: sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + '@oxlint/binding-linux-ppc64-gnu@1.47.0': resolution: {integrity: sha512-aI5ds9jq2CPDOvjeapiIj48T/vlWp+f4prkxs+FVzrmVN9BWIj0eqeJ/hV8WgXg79HVMIz9PU6deI2ki09bR1w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + resolution: {integrity: sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + '@oxlint/binding-linux-riscv64-gnu@1.47.0': resolution: {integrity: sha512-mO7ycp9Elvgt5EdGkQHCwJA6878xvo9tk+vlMfT1qg++UjvOMB8INsOCQIOH2IKErF/8/P21LULkdIrocMw9xA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + resolution: {integrity: sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + '@oxlint/binding-linux-riscv64-musl@1.47.0': resolution: {integrity: sha512-24D0wsYT/7hDFn3Ow32m3/+QT/1ZwrUhShx4/wRDAmz11GQHOZ1k+/HBuK/MflebdnalmXWITcPEy4BWTi7TCA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + '@oxlint/binding-linux-riscv64-musl@1.50.0': + resolution: {integrity: sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + '@oxlint/binding-linux-s390x-gnu@1.47.0': resolution: {integrity: sha512-8tPzPne882mtML/uy3mApvdCyuVOpthJ7xUv3b67gVfz63hOOM/bwO0cysSkPyYYFDFRn6/FnUb7Jhmsesntvg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + '@oxlint/binding-linux-s390x-gnu@1.50.0': + resolution: {integrity: sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + '@oxlint/binding-linux-x64-gnu@1.47.0': resolution: {integrity: sha512-q58pIyGIzeffEBhEgbRxLFHmHfV9m7g1RnkLiahQuEvyjKNiJcvdHOwKH2BdgZxdzc99Cs6hF5xTa86X40WzPw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@oxlint/binding-linux-x64-gnu@1.50.0': + resolution: {integrity: sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@oxlint/binding-linux-x64-musl@1.47.0': resolution: {integrity: sha512-e7DiLZtETZUCwTa4EEHg9G+7g3pY+afCWXvSeMG7m0TQ29UHHxMARPaEQUE4mfKgSqIWnJaUk2iZzRPMRdga5g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + '@oxlint/binding-linux-x64-musl@1.50.0': + resolution: {integrity: sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + '@oxlint/binding-openharmony-arm64@1.47.0': resolution: {integrity: sha512-3AFPfQ0WKMleT/bKd7zsks3xoawtZA6E/wKf0DjwysH7wUiMMJkNKXOzYq1R/00G98JFgSU1AkrlOQrSdNNhlg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxlint/binding-openharmony-arm64@1.50.0': + resolution: {integrity: sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxlint/binding-win32-arm64-msvc@1.47.0': resolution: {integrity: sha512-cLMVVM6TBxp+N7FldQJ2GQnkcLYEPGgiuEaXdvhgvSgODBk9ov3jed+khIXSAWtnFOW0wOnG3RjwqPh0rCuheA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@oxlint/binding-win32-arm64-msvc@1.50.0': + resolution: {integrity: sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxlint/binding-win32-ia32-msvc@1.47.0': resolution: {integrity: sha512-VpFOSzvTnld77/Edje3ZdHgZWnlTb5nVWXyTgjD3/DKF/6t5bRRbwn3z77zOdnGy44xAMvbyAwDNOSeOdVUmRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxlint/binding-win32-ia32-msvc@1.50.0': + resolution: {integrity: sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxlint/binding-win32-x64-msvc@1.47.0': resolution: {integrity: sha512-+q8IWptxXx2HMTM6JluR67284t0h8X/oHJgqpxH1siowxPMqZeIpAcWCUq+tY+Rv2iQK8TUugjZnSBQAVV5CmA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@oxlint/binding-win32-x64-msvc@1.50.0': + resolution: {integrity: sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -2780,16 +2891,16 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tanstack/devtools-event-client@0.3.3': - resolution: {integrity: sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg==} + '@tanstack/devtools-event-client@0.4.0': + resolution: {integrity: sha512-RPfGuk2bDZgcu9bAJodvO2lnZeHuz4/71HjZ0bGb/SPg8+lyTA+RLSKQvo7fSmPSi8/vcH3aKQ8EM9ywf1olaw==} engines: {node: '>=18'} '@tanstack/history@1.161.4': resolution: {integrity: sha512-Kp/WSt411ZWYvgXy6uiv5RmhHrz9cAml05AQPrtdAp7eUqvIDbMGPnML25OKbzR3RJ1q4wgENxDTvlGPa9+Mww==} engines: {node: '>=20.19'} - '@tanstack/pacer@0.15.4': - resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==} + '@tanstack/pacer@0.19.0': + resolution: {integrity: sha512-MRXCiG8IcjrN/3LGu7Wy6lKZkbwOb5YelOBYtHxnxKYj2WlO2FrqASILSiJcwdES5Sz2QJEIeuvO5JY8cKaGQw==} engines: {node: '>=18'} '@tanstack/query-core@5.83.1': @@ -2798,8 +2909,8 @@ packages: '@tanstack/query-core@5.90.20': resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} - '@tanstack/react-pacer@0.16.4': - resolution: {integrity: sha512-nuQLE8bx0rYMiJau4jOTPZFp3XC/GnIHDKfKVVWeKUHNF4grRdVHPgTlJ8EV/nt/HJxSUnIcy+IIKX+Bj0bLSw==} + '@tanstack/react-pacer@0.20.0': + resolution: {integrity: sha512-5p7rHTBUroUl6vxYhvREaqpUWKCoe0bXaFH6y6CLYpcuU5aCl78IxXJKY5IujCN1sTRaq07jsMInTPmTBHvTiA==} engines: {node: '>=18'} peerDependencies: react: '>=16.8' @@ -2822,8 +2933,8 @@ packages: react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' - '@tanstack/react-store@0.7.7': - resolution: {integrity: sha512-qqT0ufegFRDGSof9D/VqaZgjNgp4tRPHZIJq2+QIHkMUtHjaJ0lYrrXjeIUJvjnTbgPfSD1XgOMEt0lmANn6Zg==} + '@tanstack/react-store@0.8.1': + resolution: {integrity: sha512-XItJt+rG8c5Wn/2L/bnxys85rBpm0BfMbhb4zmPVLXAKY9POrp1xd6IbU4PKoOI+jSEGc3vntPRfLGSgXfE2Ig==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -2838,22 +2949,20 @@ packages: resolution: {integrity: sha512-WFMNysDsDtnlM0G0L4LPWJuvpGatlPvBLGlPnieWYKem/Ed4mRHu7Hqw78MR/CMuFSRi9Gvv91/h8F3EVswAJw==} engines: {node: '>=20.19'} - '@tanstack/store@0.7.7': - resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==} + '@tanstack/store@0.8.1': + resolution: {integrity: sha512-PtOisLjUZPz5VyPRSCGjNOlwTvabdTBQ2K80DpVL1chGVr35WRxfeavAPdNq6pm/t7F8GhoR2qtmkkqtCEtHYw==} '@tanstack/store@0.9.1': resolution: {integrity: sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg==} - '@transfem-org/sfm-js@0.24.8': - resolution: {integrity: sha1-G97++XwNPZZaxIExiJbm2kJZSg0=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.24.8.tgz} + '@transfem-org/sfm-js@0.26.1': + resolution: {integrity: sha1-42SS8z0rQLz7gjyg5fbNeUIHHVk=, tarball: https://activitypub.software/api/v4/projects/2/packages/npm/@transfem-org/sfm-js/-/@transfem-org/sfm-js-0.26.1.tgz} + engines: {node: ^22.0.0} '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} - '@twemoji/parser@15.0.0': - resolution: {integrity: sha512-lh9515BNsvKSNvyUqbj5yFu83iIDQ77SwVcsN/SnEGawczhsKU6qWuogewN1GweTi5Imo5ToQ9s+nNTf97IXvg==} - '@twemoji/svg@15.0.0': resolution: {integrity: sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==} @@ -2878,8 +2987,8 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/dom-chromium-ai@0.0.11': - resolution: {integrity: sha512-Li04Mac9ic1vbX/te9re8v1010fh5YB/30dMcJLpIuIyDoT7xE/dIdg9r9UrFZLs5Ztmonb3nP7+LhPpFuHBGw==} + '@types/dom-chromium-ai@0.0.14': + resolution: {integrity: sha512-inGI/sSihVWbJUt4jXZqjcdT8lee3BZHyhbBOafR4wVS8JYQ9u9XoWW6kcvJazhAyAlJ6lL7V6wxoFvnomEKTA==} '@types/emscripten@1.40.1': resolution: {integrity: sha512-sr53lnYkQNhjHNN0oJDdUm5564biioI5DuOpycufDVK7D3y+GR3oUswe2rlwY1nPNyusHbrJ9WoTyIHl4/Bpwg==} @@ -2902,9 +3011,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/history@4.7.11': - resolution: {integrity: sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==} - '@types/hoist-non-react-statics@3.3.7': resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} peerDependencies: @@ -2931,14 +3037,17 @@ packages: '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} '@types/node@22.17.0': resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==} - '@types/node@22.19.3': - resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==} + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -2946,8 +3055,8 @@ packages: '@types/path-browserify@1.0.3': resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} - '@types/picomatch@3.0.2': - resolution: {integrity: sha512-n0i8TD3UDB7paoMMxA3Y65vUncFJXjcUf7lQY7YyKGl6031FNjfsLs6pdLFCy2GNFxItPJG8GvvpbZc2skH7WA==} + '@types/picomatch@4.0.2': + resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==} '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2962,12 +3071,6 @@ packages: peerDependencies: '@types/react': ^18.3.18 - '@types/react-router-dom@5.3.3': - resolution: {integrity: sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==} - - '@types/react-router@5.1.20': - resolution: {integrity: sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==} - '@types/react-sparklines@1.7.5': resolution: {integrity: sha512-rIAmNyRKUqWWnaQMjNrxMNkgEFi5f9PrdczSNxj5DscAa48y4i9P0fRKZ72FmNcFsdg6Jx4o6CXWZtIaC0OJOg==} @@ -3021,26 +3124,10 @@ packages: typescript: optional: true - '@typescript-eslint/project-service@8.38.0': - resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@7.18.0': resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/scope-manager@8.38.0': - resolution: {integrity: sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/tsconfig-utils@8.38.0': - resolution: {integrity: sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/type-utils@7.18.0': resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -3055,10 +3142,6 @@ packages: resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/types@8.38.0': - resolution: {integrity: sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} engines: {node: ^18.18.0 || >=20.0.0} @@ -3068,33 +3151,16 @@ packages: typescript: optional: true - '@typescript-eslint/typescript-estree@8.38.0': - resolution: {integrity: sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@7.18.0': resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: eslint: ^8.56.0 - '@typescript-eslint/utils@8.38.0': - resolution: {integrity: sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@7.18.0': resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} engines: {node: ^18.18.0 || >=20.0.0} - '@typescript-eslint/visitor-keys@8.38.0': - resolution: {integrity: sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@uidotdev/usehooks@2.4.1': resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} engines: {node: '>=16'} @@ -3105,6 +3171,9 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unicode/unicode-17.0.0@1.6.16': + resolution: {integrity: sha512-advq5p36zZ+PDRUpDkWcHHR++R19kx0LYB5iG3bj0KB8mYVKg0ywS996e2bXeXxDb8XdOF7KTivcx7VkYie1pg==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} cpu: [arm] @@ -3303,8 +3372,8 @@ packages: '@yornaath/batshit-devtools@1.7.1': resolution: {integrity: sha512-AyttV1Njj5ug+XqEWY1smV45dTWMlWKtj1B8jcFYgBKUFyUlF/qEhD+iP1E5UaRYW6hQRYD9T2WNDwFTrOMWzQ==} - '@yornaath/batshit@0.11.2': - resolution: {integrity: sha512-lb4RPRp61leABYWkXT39sl7S3roFYC+lSxWOPYQxi2otlOkhmPzLDvVcMMzVuintOuTJo5FpfkZ4BxTfNUuCnA==} + '@yornaath/batshit@0.14.0': + resolution: {integrity: sha512-0I+xMi5JoRs3+qVXXhk2AmsEl43MwrG+L+VW+nqw/qQqMFtgRPszLaxhJCfsBKnjfJ0gJzTI1Q9Q9+y903HyHQ==} abortcontroller-polyfill@1.7.8: resolution: {integrity: sha512-9f1iZ2uWh92VcrU9Y8x+LdM4DLj75VE0MJB8zuF1iUnroEptStw+DQ8EQPMUdfe5k+PkB1uUfDQfWbhstH8LrQ==} @@ -3510,8 +3579,8 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.13.1: - resolution: {integrity: sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3535,9 +3604,6 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - buffer-builder@0.2.0: - resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -3695,8 +3761,8 @@ packages: core-js-compat@3.48.0: resolution: {integrity: sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==} - core-js@3.44.0: - resolution: {integrity: sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==} + core-js@3.48.0: + resolution: {integrity: sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==} cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} @@ -3777,8 +3843,8 @@ packages: resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} - cssstyle@5.3.7: - resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + cssstyle@6.1.0: + resolution: {integrity: sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==} engines: {node: '>=20'} csstype@3.1.3: @@ -3787,9 +3853,9 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - data-urls@6.0.0: - resolution: {integrity: sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==} - engines: {node: '>=20'} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} @@ -3951,10 +4017,6 @@ packages: emoji-mart@5.6.0: resolution: {integrity: sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==} - emoji-regex-xs@2.0.1: - resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} - engines: {node: '>=10.0.0'} - emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -4086,10 +4148,10 @@ packages: peerDependencies: eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-plugin-formatjs@5.4.2: - resolution: {integrity: sha512-IdJt/il0FASmk/aJDzl96Zh0tovm+KVhCbA5d+YC14gOpeFe1n6766JMi/RP9YOY9dhe6BbWEJnk9dPJwMMngw==} + eslint-plugin-formatjs@6.2.0: + resolution: {integrity: sha512-JftP9glJrS4qdviqTyZ0Kk14hcHB8AJn2FP2W7dsMugOIHDgra30mTvGjRMohivDIaFXnPGCAOv/AYm55BMUBQ==} peerDependencies: - eslint: ^9.23.0 + eslint: '9' eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} @@ -4119,10 +4181,6 @@ packages: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.2.1: - resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@8.57.1: resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4375,6 +4433,10 @@ packages: resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} engines: {node: '>=18'} + globals@17.3.0: + resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -4546,11 +4608,8 @@ packages: resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==} deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019. - intl-messageformat@10.7.18: - resolution: {integrity: sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==} - - intl-messageformat@11.0.8: - resolution: {integrity: sha512-q2Md8nj28CSkXxkBaAOWhTjQAdea24fpcZxqR1pMsCwzDYLQF68iOOPNTLgFFF+HKJKNUiJ+Mkjp0zXvG88UFA==} + intl-messageformat@11.1.2: + resolution: {integrity: sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==} intl-pluralrules@2.0.1: resolution: {integrity: sha512-astxTLzIdXPeN0K9Rumi6LfMpm3rvNO0iJE+h/k8Kr/is+wPbRe4ikyDjlLr6VTh/mEfNv8RjN+gu3KwDiuhqg==} @@ -4724,9 +4783,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-dompurify@2.35.0: - resolution: {integrity: sha512-a9+LQqylQCU8f1zmsYmg2tfrbdY2YS/Hc+xntcq/mDI2MY3Q108nq8K23BWDIg6YGC5JsUMC15fj2ZMqCzt/+A==} - engines: {node: '>=20.19.5'} + isomorphic-dompurify@3.0.0: + resolution: {integrity: sha512-5K+MYP7Nrg74+Bi+QmQGzQ/FgEOyVHWsN8MuJy5wYQxxBRxPnWsD25Tjjt5FWYhan3OQ+vNLubyNJH9dfG03lQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} isomorphic.js@0.2.5: resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} @@ -4765,8 +4824,8 @@ packages: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true - jsdom@27.4.0: - resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + jsdom@28.1.0: + resolution: {integrity: sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: canvas: ^3.0.0 @@ -5034,8 +5093,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -5118,8 +5177,8 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} - mini-css-extract-plugin@2.9.4: - resolution: {integrity: sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==} + mini-css-extract-plugin@2.10.0: + resolution: {integrity: sha512-540P2c5dYnJlyJxTaSloliZexv8rji6rY8FhQN+WF/82iHQfA23j/xtJx97L+mXOML27EqksSek/g4eK7jaL3g==} engines: {node: '>= 12.13.0'} peerDependencies: webpack: ^5.0.0 @@ -5287,6 +5346,16 @@ packages: oxlint-tsgolint: optional: true + oxlint@1.50.0: + resolution: {integrity: sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + oxlint-tsgolint: '>=0.14.1' + peerDependenciesMeta: + oxlint-tsgolint: + optional: true + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -5741,11 +5810,11 @@ packages: peerDependencies: react: 16.8 - 19 - react-intl@8.0.10: - resolution: {integrity: sha512-+8L+/kClosP/z9B1lgVKyX6f4vs1aEQXFIO4l2/xSjapzXnJI/TJ50qVTZrn1idJTtO9MqlmZn64b1JlYtMHFg==} + react-intl@8.1.3: + resolution: {integrity: sha512-eL1/d+uQdnapirynOGAriW0K9uAoyarjRGL3V9LaTRuohNSvPgCfJX06EZl5M52h/Hu7Gz7A1sD7dNHcos1lNg==} peerDependencies: '@types/react': ^18.3.18 - react: 16 || 17 || 18 || 19 + react: '19' typescript: ^5.6.0 peerDependenciesMeta: typescript: @@ -5997,117 +6066,117 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} - sass-embedded-all-unknown@1.93.3: - resolution: {integrity: sha512-3okGgnE41eg+CPLtAPletu6nQ4N0ij7AeW+Sl5Km4j29XcmqZQeFwYjHe1AlKTEgLi/UAONk1O8i8/lupeKMbw==} + sass-embedded-all-unknown@1.97.3: + resolution: {integrity: sha512-t6N46NlPuXiY3rlmG6/+1nwebOBOaLFOOVqNQOC2cJhghOD4hh2kHNQQTorCsbY9S1Kir2la1/XLBwOJfui0xg==} cpu: ['!arm', '!arm64', '!riscv64', '!x64'] - sass-embedded-android-arm64@1.93.3: - resolution: {integrity: sha512-uqUl3Kt1IqdGVAcAdbmC+NwuUJy8tM+2ZnB7/zrt6WxWVShVCRdFnWR9LT8HJr7eJN7AU8kSXxaVX/gedanPsg==} + sass-embedded-android-arm64@1.97.3: + resolution: {integrity: sha512-aiZ6iqiHsUsaDx0EFbbmmA0QgxicSxVVN3lnJJ0f1RStY0DthUkquGT5RJ4TPdaZ6ebeJWkboV4bra+CP766eA==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [android] - sass-embedded-android-arm@1.93.3: - resolution: {integrity: sha512-8xOw9bywfOD6Wv24BgCmgjkk6tMrsOTTHcb28KDxeJtFtoxiUyMbxo0vChpPAfp2Hyg2tFFKS60s0s4JYk+Raw==} + sass-embedded-android-arm@1.97.3: + resolution: {integrity: sha512-cRTtf/KV/q0nzGZoUzVkeIVVFv3L/tS1w4WnlHapphsjTXF/duTxI8JOU1c/9GhRPiMdfeXH7vYNcMmtjwX7jg==} engines: {node: '>=14.0.0'} cpu: [arm] os: [android] - sass-embedded-android-riscv64@1.93.3: - resolution: {integrity: sha512-2jNJDmo+3qLocjWqYbXiBDnfgwrUeZgZFHJIwAefU7Fn66Ot7rsXl+XPwlokaCbTpj7eMFIqsRAZ/uDueXNCJg==} + sass-embedded-android-riscv64@1.97.3: + resolution: {integrity: sha512-zVEDgl9JJodofGHobaM/q6pNETG69uuBIGQHRo789jloESxxZe82lI3AWJQuPmYCOG5ElfRthqgv89h3gTeLYA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [android] - sass-embedded-android-x64@1.93.3: - resolution: {integrity: sha512-y0RoAU6ZenQFcjM9PjQd3cRqRTjqwSbtWLL/p68y2oFyh0QGN0+LQ826fc0ZvU/AbqCsAizkqjzOn6cRZJxTTQ==} + sass-embedded-android-x64@1.97.3: + resolution: {integrity: sha512-3ke0le7ZKepyXn/dKKspYkpBC0zUk/BMciyP5ajQUDy4qJwobd8zXdAq6kOkdiMB+d9UFJOmEkvgFJHl3lqwcw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [android] - sass-embedded-darwin-arm64@1.93.3: - resolution: {integrity: sha512-7zb/hpdMOdKteK17BOyyypemglVURd1Hdz6QGsggy60aUFfptTLQftLRg8r/xh1RbQAUKWFbYTNaM47J9yPxYg==} + sass-embedded-darwin-arm64@1.97.3: + resolution: {integrity: sha512-fuqMTqO4gbOmA/kC5b9y9xxNYw6zDEyfOtMgabS7Mz93wimSk2M1quQaTJnL98Mkcsl2j+7shNHxIS/qpcIDDA==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [darwin] - sass-embedded-darwin-x64@1.93.3: - resolution: {integrity: sha512-Ek1Vp8ZDQEe327Lz0b7h3hjvWH3u9XjJiQzveq74RPpJQ2q6d9LfWpjiRRohM4qK6o4XOHw1X10OMWPXJtdtWg==} + sass-embedded-darwin-x64@1.97.3: + resolution: {integrity: sha512-b/2RBs/2bZpP8lMkyZ0Px0vkVkT8uBd0YXpOwK7iOwYkAT8SsO4+WdVwErsqC65vI5e1e5p1bb20tuwsoQBMVA==} engines: {node: '>=14.0.0'} cpu: [x64] os: [darwin] - sass-embedded-linux-arm64@1.93.3: - resolution: {integrity: sha512-RBrHWgfd8Dd8w4fbmdRVXRrhh8oBAPyeWDTKAWw8ZEmuXfVl4ytjDuyxaVilh6rR1xTRTNpbaA/YWApBlLrrNw==} + sass-embedded-linux-arm64@1.97.3: + resolution: {integrity: sha512-IP1+2otCT3DuV46ooxPaOKV1oL5rLjteRzf8ldZtfIEcwhSgSsHgA71CbjYgLEwMY9h4jeal8Jfv3QnedPvSjg==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-arm@1.93.3: - resolution: {integrity: sha512-yeiv2y+dp8B4wNpd3+JsHYD0mvpXSfov7IGyQ1tMIR40qv+ROkRqYiqQvAOXf76Qwh4Y9OaYZtLpnsPjfeq6mA==} + sass-embedded-linux-arm@1.97.3: + resolution: {integrity: sha512-2lPQ7HQQg4CKsH18FTsj2hbw5GJa6sBQgDsls+cV7buXlHjqF8iTKhAQViT6nrpLK/e8nFCoaRgSqEC8xMnXuA==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-musl-arm64@1.93.3: - resolution: {integrity: sha512-PS829l+eUng+9W4PFclXGb4uA2+965NHV3/Sa5U7qTywjeeUUYTZg70dJHSqvhrBEfCc2XJABeW3adLJbyQYkw==} + sass-embedded-linux-musl-arm64@1.97.3: + resolution: {integrity: sha512-Lij0SdZCsr+mNRSyDZ7XtJpXEITrYsaGbOTz5e6uFLJ9bmzUbV7M8BXz2/cA7bhfpRPT7/lwRKPdV4+aR9Ozcw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [linux] - sass-embedded-linux-musl-arm@1.93.3: - resolution: {integrity: sha512-fU0fwAwbp7sBE3h5DVU5UPzvaLg7a4yONfFWkkcCp6ZrOiPuGRHXXYriWQ0TUnWy4wE+svsVuWhwWgvlb/tkKg==} + sass-embedded-linux-musl-arm@1.97.3: + resolution: {integrity: sha512-cBTMU68X2opBpoYsSZnI321gnoaiMBEtc+60CKCclN6PCL3W3uXm8g4TLoil1hDD6mqU9YYNlVG6sJ+ZNef6Lg==} engines: {node: '>=14.0.0'} cpu: [arm] os: [linux] - sass-embedded-linux-musl-riscv64@1.93.3: - resolution: {integrity: sha512-cK1oBY+FWQquaIGEeQ5H74KTO8cWsSWwXb/WaildOO9U6wmUypTgUYKQ0o5o/29nZbWWlM1PHuwVYTSnT23Jjg==} + sass-embedded-linux-musl-riscv64@1.97.3: + resolution: {integrity: sha512-sBeLFIzMGshR4WmHAD4oIM7WJVkSoCIEwutzptFtGlSlwfNiijULp+J5hA2KteGvI6Gji35apR5aWj66wEn/iA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-musl-x64@1.93.3: - resolution: {integrity: sha512-A7wkrsHu2/I4Zpa0NMuPGkWDVV7QGGytxGyUq3opSXgAexHo/vBPlGoDXoRlSdex0cV+aTMRPjoGIfdmNlHwyg==} + sass-embedded-linux-musl-x64@1.97.3: + resolution: {integrity: sha512-/oWJ+OVrDg7ADDQxRLC/4g1+Nsz1g4mkYS2t6XmyMJKFTFK50FVI2t5sOdFH+zmMp+nXHKM036W94y9m4jjEcw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-linux-riscv64@1.93.3: - resolution: {integrity: sha512-vWkW1+HTF5qcaHa6hO80gx/QfB6GGjJUP0xLbnAoY4pwEnw5ulGv6RM8qYr8IDhWfVt/KH+lhJ2ZFxnJareisQ==} + sass-embedded-linux-riscv64@1.97.3: + resolution: {integrity: sha512-l3IfySApLVYdNx0Kjm7Zehte1CDPZVcldma3dZt+TfzvlAEerM6YDgsk5XEj3L8eHBCgHgF4A0MJspHEo2WNfA==} engines: {node: '>=14.0.0'} cpu: [riscv64] os: [linux] - sass-embedded-linux-x64@1.93.3: - resolution: {integrity: sha512-k6uFxs+e5jSuk1Y0niCwuq42F9ZC5UEP7P+RIOurIm8w/5QFa0+YqeW+BPWEW5M1FqVOsNZH3qGn4ahqvAEjPA==} + sass-embedded-linux-x64@1.97.3: + resolution: {integrity: sha512-Kwqwc/jSSlcpRjULAOVbndqEy2GBzo6OBmmuBVINWUaJLJ8Kczz3vIsDUWLfWz/kTEw9FHBSiL0WCtYLVAXSLg==} engines: {node: '>=14.0.0'} cpu: [x64] os: [linux] - sass-embedded-unknown-all@1.93.3: - resolution: {integrity: sha512-o5wj2rLpXH0C+GJKt/VpWp6AnMsCCbfFmnMAttcrsa+U3yrs/guhZ3x55KAqqUsE8F47e3frbsDL+1OuQM5DAA==} + sass-embedded-unknown-all@1.97.3: + resolution: {integrity: sha512-/GHajyYJmvb0IABUQHbVHf1nuHPtIDo/ClMZ81IDr59wT5CNcMe7/dMNujXwWugtQVGI5UGmqXWZQCeoGnct8Q==} os: ['!android', '!darwin', '!linux', '!win32'] - sass-embedded-win32-arm64@1.93.3: - resolution: {integrity: sha512-0dOfT9moy9YmBolodwYYXtLwNr4jL4HQC9rBfv6mVrD7ud8ue2kDbn+GVzj1hEJxvEexVSmDCf7MHUTLcGs9xQ==} + sass-embedded-win32-arm64@1.97.3: + resolution: {integrity: sha512-RDGtRS1GVvQfMGAmVXNxYiUOvPzn9oO1zYB/XUM9fudDRnieYTcUytpNTQZLs6Y1KfJxgt5Y+giRceC92fT8Uw==} engines: {node: '>=14.0.0'} cpu: [arm64] os: [win32] - sass-embedded-win32-x64@1.93.3: - resolution: {integrity: sha512-wHFVfxiS9hU/sNk7KReD+lJWRp3R0SLQEX4zfOnRP2zlvI2X4IQR5aZr9GNcuMP6TmNpX0nQPZTegS8+h9RrEg==} + sass-embedded-win32-x64@1.97.3: + resolution: {integrity: sha512-SFRa2lED9UEwV6vIGeBXeBOLKF+rowF3WmNfb/BzhxmdAsKofCXrJ8ePW7OcDVrvNEbTOGwhsReIsF5sH8fVaw==} engines: {node: '>=14.0.0'} cpu: [x64] os: [win32] - sass-embedded@1.93.3: - resolution: {integrity: sha512-+VUy01yfDqNmIVMd/LLKl2TTtY0ovZN0rTonh+FhKr65mFwIYgU9WzgIZKS7U9/SPCQvWTsTGx9jyt+qRm/XFw==} + sass-embedded@1.97.3: + resolution: {integrity: sha512-eKzFy13Nk+IRHhlAwP3sfuv+PzOrvzUkwJK2hdoCKYcWGSdmwFpeGpWmyewdw8EgBnsKaSBtgf/0b2K635ecSA==} engines: {node: '>=16.0.0'} hasBin: true - sass@1.93.3: - resolution: {integrity: sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==} + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} engines: {node: '>=14.0.0'} hasBin: true @@ -6433,8 +6502,8 @@ packages: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@3.4.17: - resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} + tailwindcss@3.4.19: + resolution: {integrity: sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==} engines: {node: '>=14.0.0'} hasBin: true @@ -6538,12 +6607,6 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} @@ -6603,11 +6666,6 @@ packages: peerDependencies: typescript: 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x - typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.8.2: resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} @@ -6618,6 +6676,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -6634,13 +6697,14 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@7.22.0: + resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} + engines: {node: '>=20.18.1'} + unicode-canonical-property-names-ecmascript@2.0.1: resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} engines: {node: '>=4'} - unicode-emoji-utils@1.3.1: - resolution: {integrity: sha512-6PiQxmnlsOsqzZCZz0sykSyMy/r1HiJiOWWXV98+BDva583DU4CtBeyDNsi4wMYUIbjUtMs4RgAuyft0EKLoVw==} - unicode-match-property-ecmascript@2.0.0: resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} engines: {node: '>=4'} @@ -6928,14 +6992,14 @@ packages: webpack-cli: optional: true - whatwg-mimetype@4.0.0: - resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} - engines: {node: '>=18'} - - whatwg-url@15.1.0: - resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} engines: {node: '>=20'} + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -7048,18 +7112,6 @@ packages: utf-8-validate: optional: true - ws@8.19.0: - resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -7114,7 +7166,7 @@ packages: snapshots: - '@acemir/cssom@0.9.30': {} + '@acemir/cssom@0.9.31': {} '@alloc/quick-lru@5.2.0': {} @@ -7125,21 +7177,21 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 - '@asamuzakjp/css-color@4.1.1': + '@asamuzakjp/css-color@5.0.1': dependencies: - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.4 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.2.6 - '@asamuzakjp/dom-selector@6.7.6': + '@asamuzakjp/dom-selector@6.8.1': dependencies: '@asamuzakjp/nwsapi': 2.3.9 bidi-js: 1.0.3 css-tree: 3.1.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.4 + lru-cache: 11.2.6 '@asamuzakjp/nwsapi@2.3.9': {} @@ -7865,13 +7917,17 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.1.0 + '@bufbuild/protobuf@2.10.1': {} - '@bundle-stats/cli-utils@4.21.10(core-js@3.44.0)': + '@bundle-stats/cli-utils@4.21.10(core-js@3.48.0)': dependencies: '@bundle-stats/html-templates': 4.21.10 - '@bundle-stats/plugin-webpack-filter': 4.21.10(core-js@3.44.0) - '@bundle-stats/utils': 4.21.10(core-js@3.44.0)(lodash@4.17.23) + '@bundle-stats/plugin-webpack-filter': 4.21.10(core-js@3.48.0) + '@bundle-stats/utils': 4.21.10(core-js@3.48.0)(lodash@4.17.23) find-cache-dir: 3.3.2 lodash: 4.17.23 stream-chain: 3.4.0 @@ -7881,9 +7937,9 @@ snapshots: '@bundle-stats/html-templates@4.21.10': {} - '@bundle-stats/plugin-webpack-filter@4.21.10(core-js@3.44.0)': + '@bundle-stats/plugin-webpack-filter@4.21.10(core-js@3.48.0)': dependencies: - core-js: 3.44.0 + core-js: 3.48.0 tslib: 2.8.1 '@bundle-stats/plugin-webpack-validate@4.21.10': @@ -7892,36 +7948,42 @@ snapshots: superstruct: 2.0.2 tslib: 2.8.1 - '@bundle-stats/utils@4.21.10(core-js@3.44.0)(lodash@4.17.23)': + '@bundle-stats/utils@4.21.10(core-js@3.48.0)(lodash@4.17.23)': dependencies: - '@bundle-stats/plugin-webpack-filter': 4.21.10(core-js@3.44.0) + '@bundle-stats/plugin-webpack-filter': 4.21.10(core-js@3.48.0) '@bundle-stats/plugin-webpack-validate': 4.21.10 - core-js: 3.44.0 + core-js: 3.48.0 lodash: 4.17.23 serialize-query-params: 2.0.4 - '@csstools/color-helpers@5.1.0': {} + '@csstools/color-helpers@6.0.2': {} - '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 - '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + '@csstools/css-color-parser@4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': dependencies: - '@csstools/color-helpers': 5.1.0 - '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) - '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) - '@csstools/css-tokenizer': 3.0.4 + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-tokenizer': 3.0.4 - '@csstools/css-syntax-patches-for-csstree@1.0.22': {} + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.0.28': {} '@csstools/css-tokenizer@3.0.4': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/media-query-list-parser@4.0.3(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) @@ -8143,26 +8205,26 @@ snapshots: '@eslint/js@8.57.1': {} - '@exodus/bytes@1.8.0': {} + '@exodus/bytes@1.14.1': {} - '@floating-ui/core@1.7.3': + '@floating-ui/core@1.7.4': dependencies: '@floating-ui/utils': 0.2.10 - '@floating-ui/dom@1.7.4': + '@floating-ui/dom@1.7.5': dependencies: - '@floating-ui/core': 1.7.3 + '@floating-ui/core': 1.7.4 '@floating-ui/utils': 0.2.10 - '@floating-ui/react-dom@2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/dom': 1.7.4 + '@floating-ui/dom': 1.7.5 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@floating-ui/react@0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react@0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@floating-ui/react-dom': 2.1.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -8180,77 +8242,50 @@ snapshots: '@formatjs/cli@6.13.0': {} - '@formatjs/ecma402-abstract@2.3.6': + '@formatjs/ecma402-abstract@3.1.1': dependencies: - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/intl-localematcher': 0.6.2 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/intl-localematcher': 0.8.1 decimal.js: 10.6.0 tslib: 2.8.1 - '@formatjs/ecma402-abstract@3.0.7': - dependencies: - '@formatjs/fast-memoize': 3.0.2 - '@formatjs/intl-localematcher': 0.7.4 - decimal.js: 10.6.0 - tslib: 2.8.1 - - '@formatjs/fast-memoize@2.2.7': + '@formatjs/fast-memoize@3.1.0': dependencies: tslib: 2.8.1 - '@formatjs/fast-memoize@3.0.2': + '@formatjs/icu-messageformat-parser@3.5.1': dependencies: + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-skeleton-parser': 2.1.1 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@2.11.4': + '@formatjs/icu-skeleton-parser@2.1.1': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/icu-skeleton-parser': 1.8.16 + '@formatjs/ecma402-abstract': 3.1.1 tslib: 2.8.1 - '@formatjs/icu-messageformat-parser@3.2.1': + '@formatjs/intl-localematcher@0.8.1': dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/icu-skeleton-parser': 2.0.7 + '@formatjs/fast-memoize': 3.1.0 tslib: 2.8.1 - '@formatjs/icu-skeleton-parser@1.8.16': + '@formatjs/intl@4.1.2(typescript@5.9.3)': dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - tslib: 2.8.1 - - '@formatjs/icu-skeleton-parser@2.0.7': - dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.6.2': - dependencies: - tslib: 2.8.1 - - '@formatjs/intl-localematcher@0.7.4': - dependencies: - '@formatjs/fast-memoize': 3.0.2 - tslib: 2.8.1 - - '@formatjs/intl@4.0.8(typescript@5.7.3)': - dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/fast-memoize': 3.0.2 - '@formatjs/icu-messageformat-parser': 3.2.1 - intl-messageformat: 11.0.8 + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 + intl-messageformat: 11.1.2 tslib: 2.8.1 optionalDependencies: - typescript: 5.7.3 + typescript: 5.9.3 - '@formatjs/ts-transformer@3.14.2': + '@formatjs/ts-transformer@4.4.0': dependencies: - '@formatjs/icu-messageformat-parser': 2.11.4 - '@types/node': 22.19.3 - chalk: 4.1.2 + '@formatjs/icu-messageformat-parser': 3.5.1 + '@types/node': 22.19.11 json-stable-stringify: 1.3.0 tslib: 2.8.1 - typescript: 5.7.3 + typescript: 5.9.3 '@gerrit0/mini-shiki@3.9.1': dependencies: @@ -8434,7 +8469,7 @@ snapshots: '@lexical/react@0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.27)': dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@lexical/devtools-core': 0.40.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@lexical/dragon': 0.40.0 '@lexical/extension': 0.40.0 @@ -8727,60 +8762,117 @@ snapshots: '@oxlint/binding-android-arm-eabi@1.47.0': optional: true + '@oxlint/binding-android-arm-eabi@1.50.0': + optional: true + '@oxlint/binding-android-arm64@1.47.0': optional: true + '@oxlint/binding-android-arm64@1.50.0': + optional: true + '@oxlint/binding-darwin-arm64@1.47.0': optional: true + '@oxlint/binding-darwin-arm64@1.50.0': + optional: true + '@oxlint/binding-darwin-x64@1.47.0': optional: true + '@oxlint/binding-darwin-x64@1.50.0': + optional: true + '@oxlint/binding-freebsd-x64@1.47.0': optional: true + '@oxlint/binding-freebsd-x64@1.50.0': + optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.47.0': optional: true + '@oxlint/binding-linux-arm-gnueabihf@1.50.0': + optional: true + '@oxlint/binding-linux-arm-musleabihf@1.47.0': optional: true + '@oxlint/binding-linux-arm-musleabihf@1.50.0': + optional: true + '@oxlint/binding-linux-arm64-gnu@1.47.0': optional: true + '@oxlint/binding-linux-arm64-gnu@1.50.0': + optional: true + '@oxlint/binding-linux-arm64-musl@1.47.0': optional: true + '@oxlint/binding-linux-arm64-musl@1.50.0': + optional: true + '@oxlint/binding-linux-ppc64-gnu@1.47.0': optional: true + '@oxlint/binding-linux-ppc64-gnu@1.50.0': + optional: true + '@oxlint/binding-linux-riscv64-gnu@1.47.0': optional: true + '@oxlint/binding-linux-riscv64-gnu@1.50.0': + optional: true + '@oxlint/binding-linux-riscv64-musl@1.47.0': optional: true + '@oxlint/binding-linux-riscv64-musl@1.50.0': + optional: true + '@oxlint/binding-linux-s390x-gnu@1.47.0': optional: true + '@oxlint/binding-linux-s390x-gnu@1.50.0': + optional: true + '@oxlint/binding-linux-x64-gnu@1.47.0': optional: true + '@oxlint/binding-linux-x64-gnu@1.50.0': + optional: true + '@oxlint/binding-linux-x64-musl@1.47.0': optional: true + '@oxlint/binding-linux-x64-musl@1.50.0': + optional: true + '@oxlint/binding-openharmony-arm64@1.47.0': optional: true + '@oxlint/binding-openharmony-arm64@1.50.0': + optional: true + '@oxlint/binding-win32-arm64-msvc@1.47.0': optional: true + '@oxlint/binding-win32-arm64-msvc@1.50.0': + optional: true + '@oxlint/binding-win32-ia32-msvc@1.47.0': optional: true + '@oxlint/binding-win32-ia32-msvc@1.50.0': + optional: true + '@oxlint/binding-win32-x64-msvc@1.47.0': optional: true + '@oxlint/binding-win32-x64-msvc@1.50.0': + optional: true + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -9277,40 +9369,40 @@ snapshots: magic-string: 0.25.9 string.prototype.matchall: 4.0.12 - '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.17)': + '@tailwindcss/aspect-ratio@0.4.2(tailwindcss@3.4.19)': dependencies: - tailwindcss: 3.4.17 + tailwindcss: 3.4.19 - '@tailwindcss/forms@0.5.10(tailwindcss@3.4.17)': + '@tailwindcss/forms@0.5.10(tailwindcss@3.4.19)': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.17 + tailwindcss: 3.4.19 - '@tailwindcss/typography@0.5.16(tailwindcss@3.4.17)': + '@tailwindcss/typography@0.5.16(tailwindcss@3.4.19)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.17 + tailwindcss: 3.4.19 - '@tanstack/devtools-event-client@0.3.3': {} + '@tanstack/devtools-event-client@0.4.0': {} '@tanstack/history@1.161.4': {} - '@tanstack/pacer@0.15.4': + '@tanstack/pacer@0.19.0': dependencies: - '@tanstack/devtools-event-client': 0.3.3 - '@tanstack/store': 0.7.7 + '@tanstack/devtools-event-client': 0.4.0 + '@tanstack/store': 0.8.1 '@tanstack/query-core@5.83.1': {} '@tanstack/query-core@5.90.20': {} - '@tanstack/react-pacer@0.16.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-pacer@0.20.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/pacer': 0.15.4 - '@tanstack/react-store': 0.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@tanstack/pacer': 0.19.0 + '@tanstack/react-store': 0.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) @@ -9335,12 +9427,12 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/react-store@0.7.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-store@0.8.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@tanstack/store': 0.7.7 + '@tanstack/store': 0.8.1 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.5.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.4) '@tanstack/react-store@0.9.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: @@ -9359,18 +9451,14 @@ snapshots: tiny-invariant: 1.3.3 tiny-warning: 1.0.3 - '@tanstack/store@0.7.7': {} + '@tanstack/store@0.8.1': {} '@tanstack/store@0.9.1': {} - '@transfem-org/sfm-js@0.24.8': - dependencies: - '@twemoji/parser': 15.0.0 + '@transfem-org/sfm-js@0.26.1': {} '@trysound/sax@0.2.0': {} - '@twemoji/parser@15.0.0': {} - '@twemoji/svg@15.0.0': {} '@tybys/wasm-util@0.10.0': @@ -9406,7 +9494,7 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@types/dom-chromium-ai@0.0.11': {} + '@types/dom-chromium-ai@0.0.14': {} '@types/emscripten@1.40.1': {} @@ -9430,8 +9518,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/history@4.7.11': {} - '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.27)': dependencies: '@types/react': 18.3.27 @@ -9459,6 +9545,8 @@ snapshots: '@types/lodash@4.17.20': {} + '@types/lodash@4.17.24': {} + '@types/node@20.19.9': dependencies: undici-types: 6.21.0 @@ -9467,7 +9555,7 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.19.3': + '@types/node@22.19.11': dependencies: undici-types: 6.21.0 @@ -9477,7 +9565,7 @@ snapshots: '@types/path-browserify@1.0.3': {} - '@types/picomatch@3.0.2': {} + '@types/picomatch@4.0.2': {} '@types/prop-types@15.7.15': {} @@ -9490,17 +9578,6 @@ snapshots: dependencies: '@types/react': 18.3.27 - '@types/react-router-dom@5.3.3': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.3.27 - '@types/react-router': 5.1.20 - - '@types/react-router@5.1.20': - dependencies: - '@types/history': 4.7.11 - '@types/react': 18.3.27 - '@types/react-sparklines@1.7.5': dependencies: '@types/react': 18.3.27 @@ -9564,29 +9641,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.38.0(typescript@5.7.3)': - dependencies: - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.7.3) - '@typescript-eslint/types': 8.38.0 - debug: 4.4.3 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/scope-manager@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 '@typescript-eslint/visitor-keys': 7.18.0 - '@typescript-eslint/scope-manager@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - - '@typescript-eslint/tsconfig-utils@8.38.0(typescript@5.7.3)': - dependencies: - typescript: 5.7.3 - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) @@ -9601,8 +9660,6 @@ snapshots: '@typescript-eslint/types@7.18.0': {} - '@typescript-eslint/types@8.38.0': {} - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 7.18.0 @@ -9618,22 +9675,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.38.0(typescript@5.7.3)': - dependencies: - '@typescript-eslint/project-service': 8.38.0(typescript@5.7.3) - '@typescript-eslint/tsconfig-utils': 8.38.0(typescript@5.7.3) - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/visitor-keys': 8.38.0 - debug: 4.4.1 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.2 - ts-api-utils: 2.1.0(typescript@5.7.3) - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) @@ -9645,27 +9686,11 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@8.38.0(eslint@8.57.1)(typescript@5.7.3)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 8.38.0 - '@typescript-eslint/types': 8.38.0 - '@typescript-eslint/typescript-estree': 8.38.0(typescript@5.7.3) - eslint: 8.57.1 - typescript: 5.7.3 - transitivePeerDependencies: - - supports-color - '@typescript-eslint/visitor-keys@7.18.0': dependencies: '@typescript-eslint/types': 7.18.0 eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.38.0': - dependencies: - '@typescript-eslint/types': 8.38.0 - eslint-visitor-keys: 4.2.1 - '@uidotdev/usehooks@2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: react: 19.2.4 @@ -9673,6 +9698,8 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@unicode/unicode-17.0.0@1.6.16': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': optional: true @@ -9739,7 +9766,7 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.4 - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -9747,7 +9774,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color @@ -9895,7 +9922,7 @@ snapshots: '@yornaath/batshit-devtools@1.7.1': {} - '@yornaath/batshit@0.11.2': + '@yornaath/batshit@0.14.0': dependencies: '@yornaath/batshit-devtools': 1.7.1 @@ -9917,17 +9944,17 @@ snapshots: optionalDependencies: ajv: 8.13.0 - ajv-formats@2.1.1(ajv@8.17.1): + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: - ajv: 8.17.1 + ajv: 8.18.0 ajv-formats@3.0.1(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 - ajv-keywords@5.1.0(ajv@8.17.1): + ajv-keywords@5.1.0(ajv@8.18.0): dependencies: - ajv: 8.17.1 + ajv: 8.18.0 fast-deep-equal: 3.1.3 ajv@6.12.6: @@ -10122,7 +10149,7 @@ snapshots: boolbase@1.0.0: {} - bowser@2.13.1: {} + bowser@2.14.1: {} brace-expansion@1.1.12: dependencies: @@ -10150,8 +10177,6 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) - buffer-builder@0.2.0: {} - buffer-from@1.1.2: {} bundle-require@5.1.0(esbuild@0.24.2): @@ -10291,16 +10316,16 @@ snapshots: dependencies: browserslist: 4.28.1 - core-js@3.44.0: {} + core-js@3.48.0: {} - cosmiconfig@9.0.0(typescript@5.7.3): + cosmiconfig@9.0.0(typescript@5.9.3): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.1.0 parse-json: 5.2.0 optionalDependencies: - typescript: 5.7.3 + typescript: 5.9.3 cross-spawn@7.0.6: dependencies: @@ -10401,21 +10426,23 @@ snapshots: dependencies: css-tree: 2.2.1 - cssstyle@5.3.7: + cssstyle@6.1.0: dependencies: - '@asamuzakjp/css-color': 4.1.1 - '@csstools/css-syntax-patches-for-csstree': 1.0.22 + '@asamuzakjp/css-color': 5.0.1 + '@csstools/css-syntax-patches-for-csstree': 1.0.28 css-tree: 3.1.0 - lru-cache: 11.2.4 + lru-cache: 11.2.6 csstype@3.1.3: {} csstype@3.2.3: {} - data-urls@6.0.0: + data-urls@7.0.0: dependencies: - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' data-view-buffer@1.0.2: dependencies: @@ -10565,8 +10592,6 @@ snapshots: emoji-mart@5.6.0: {} - emoji-regex-xs@2.0.1: {} - emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -10785,22 +10810,18 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.2 - eslint-plugin-formatjs@5.4.2(eslint@8.57.1)(typescript@5.7.3): + eslint-plugin-formatjs@6.2.0(eslint@8.57.1): dependencies: - '@formatjs/icu-messageformat-parser': 2.11.4 - '@formatjs/ts-transformer': 3.14.2 - '@types/eslint': 9.6.1 - '@types/picomatch': 3.0.2 - '@typescript-eslint/utils': 8.38.0(eslint@8.57.1)(typescript@5.7.3) + '@formatjs/icu-messageformat-parser': 3.5.1 + '@formatjs/ts-transformer': 4.4.0 + '@types/picomatch': 4.0.2 + '@unicode/unicode-17.0.0': 1.6.16 eslint: 8.57.1 magic-string: 0.30.17 picomatch: 4.0.3 tslib: 2.8.1 - unicode-emoji-utils: 1.3.1 transitivePeerDependencies: - - supports-color - ts-jest - - typescript eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: @@ -10847,8 +10868,6 @@ snapshots: eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.2.1: {} - eslint@8.57.1: dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) @@ -11150,6 +11169,8 @@ snapshots: globals@15.15.0: {} + globals@17.3.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -11220,9 +11241,9 @@ snapshots: html-encoding-sniffer@6.0.0: dependencies: - '@exodus/bytes': 1.8.0 + '@exodus/bytes': 1.14.1 transitivePeerDependencies: - - '@exodus/crypto' + - '@noble/hashes' html-minifier-terser@6.1.0: dependencies: @@ -11258,14 +11279,14 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 - debug: 4.4.1 + debug: 4.4.3 transitivePeerDependencies: - supports-color @@ -11313,18 +11334,11 @@ snapshots: intersection-observer@0.12.2: {} - intl-messageformat@10.7.18: + intl-messageformat@11.1.2: dependencies: - '@formatjs/ecma402-abstract': 2.3.6 - '@formatjs/fast-memoize': 2.2.7 - '@formatjs/icu-messageformat-parser': 2.11.4 - tslib: 2.8.1 - - intl-messageformat@11.0.8: - dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/fast-memoize': 3.0.2 - '@formatjs/icu-messageformat-parser': 3.2.1 + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/fast-memoize': 3.1.0 + '@formatjs/icu-messageformat-parser': 3.5.1 tslib: 2.8.1 intl-pluralrules@2.0.1: {} @@ -11485,16 +11499,14 @@ snapshots: isexe@2.0.0: {} - isomorphic-dompurify@2.35.0: + isomorphic-dompurify@3.0.0: dependencies: dompurify: 3.3.1 - jsdom: 27.4.0 + jsdom: 28.1.0 transitivePeerDependencies: - - '@exodus/crypto' - - bufferutil + - '@noble/hashes' - canvas - supports-color - - utf-8-validate isomorphic.js@0.2.5: {} @@ -11532,13 +11544,14 @@ snapshots: dependencies: argparse: 2.0.1 - jsdom@27.4.0: + jsdom@28.1.0: dependencies: - '@acemir/cssom': 0.9.30 - '@asamuzakjp/dom-selector': 6.7.6 - '@exodus/bytes': 1.8.0 - cssstyle: 5.3.7 - data-urls: 6.0.0 + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@bramus/specificity': 2.4.2 + '@exodus/bytes': 1.14.1 + cssstyle: 6.1.0 + data-urls: 7.0.0 decimal.js: 10.6.0 html-encoding-sniffer: 6.0.0 http-proxy-agent: 7.0.2 @@ -11548,17 +11561,15 @@ snapshots: saxes: 6.0.0 symbol-tree: 3.2.4 tough-cookie: 6.0.0 + undici: 7.22.0 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 - whatwg-mimetype: 4.0.0 - whatwg-url: 15.1.0 - ws: 8.19.0 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 xml-name-validator: 5.0.0 transitivePeerDependencies: - - '@exodus/crypto' - - bufferutil + - '@noble/hashes' - supports-color - - utf-8-validate jsesc@3.1.0: {} @@ -11788,7 +11799,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} + lru-cache@11.2.6: {} lru-cache@5.1.1: dependencies: @@ -11858,7 +11869,7 @@ snapshots: mimic-function@5.0.1: {} - mini-css-extract-plugin@2.9.4(webpack@5.101.0(esbuild@0.24.2)): + mini-css-extract-plugin@2.10.0(webpack@5.101.0(esbuild@0.24.2)): dependencies: schema-utils: 4.3.2 tapable: 2.2.2 @@ -12089,6 +12100,29 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.47.0 oxlint-tsgolint: 0.14.2 + oxlint@1.50.0(oxlint-tsgolint@0.14.2): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.50.0 + '@oxlint/binding-android-arm64': 1.50.0 + '@oxlint/binding-darwin-arm64': 1.50.0 + '@oxlint/binding-darwin-x64': 1.50.0 + '@oxlint/binding-freebsd-x64': 1.50.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.50.0 + '@oxlint/binding-linux-arm-musleabihf': 1.50.0 + '@oxlint/binding-linux-arm64-gnu': 1.50.0 + '@oxlint/binding-linux-arm64-musl': 1.50.0 + '@oxlint/binding-linux-ppc64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-gnu': 1.50.0 + '@oxlint/binding-linux-riscv64-musl': 1.50.0 + '@oxlint/binding-linux-s390x-gnu': 1.50.0 + '@oxlint/binding-linux-x64-gnu': 1.50.0 + '@oxlint/binding-linux-x64-musl': 1.50.0 + '@oxlint/binding-openharmony-arm64': 1.50.0 + '@oxlint/binding-win32-arm64-msvc': 1.50.0 + '@oxlint/binding-win32-ia32-msvc': 1.50.0 + '@oxlint/binding-win32-x64-msvc': 1.50.0 + oxlint-tsgolint: 0.14.2 + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -12232,7 +12266,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 read-cache: 1.0.0 - resolve: 1.22.10 + resolve: 1.22.11 postcss-js@4.0.1(postcss@8.5.6): dependencies: @@ -12454,7 +12488,7 @@ snapshots: react-datepicker@8.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - '@floating-ui/react': 0.27.16(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react': 0.27.18(react-dom@19.2.4(react@19.2.4))(react@19.2.4) clsx: 2.1.1 date-fns: 4.1.0 react: 19.2.4 @@ -12502,19 +12536,19 @@ snapshots: react: 19.2.4 react-from-dom: 0.7.5(react@19.2.4) - react-intl@8.0.10(@types/react@18.3.27)(react@19.2.4)(typescript@5.7.3): + react-intl@8.1.3(@types/react@18.3.27)(react@19.2.4)(typescript@5.9.3): dependencies: - '@formatjs/ecma402-abstract': 3.0.7 - '@formatjs/icu-messageformat-parser': 3.2.1 - '@formatjs/intl': 4.0.8(typescript@5.7.3) + '@formatjs/ecma402-abstract': 3.1.1 + '@formatjs/icu-messageformat-parser': 3.5.1 + '@formatjs/intl': 4.1.2(typescript@5.9.3) '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.27) '@types/react': 18.3.27 hoist-non-react-statics: 3.3.2 - intl-messageformat: 11.0.8 + intl-messageformat: 11.1.2 react: 19.2.4 tslib: 2.8.1 optionalDependencies: - typescript: 5.7.3 + typescript: 5.9.3 react-is@16.13.1: {} @@ -12699,31 +12733,31 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4 - rollup-plugin-bundle-stats@4.21.10(core-js@3.44.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-bundle-stats@4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: - '@bundle-stats/cli-utils': 4.21.10(core-js@3.44.0) - rollup-plugin-webpack-stats: 2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + '@bundle-stats/cli-utils': 4.21.10(core-js@3.48.0) + rollup-plugin-webpack-stats: 2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) tslib: 2.8.1 optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) transitivePeerDependencies: - core-js - rollup-plugin-stats@1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-stats@1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) - rollup-plugin-webpack-stats@2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-webpack-stats@2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: - rollup-plugin-stats: 1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)) + rollup-plugin-stats: 1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) rollup@2.79.2: optionalDependencies: @@ -12789,68 +12823,67 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 - sass-embedded-all-unknown@1.93.3: + sass-embedded-all-unknown@1.97.3: dependencies: - sass: 1.93.3 + sass: 1.97.3 optional: true - sass-embedded-android-arm64@1.93.3: + sass-embedded-android-arm64@1.97.3: optional: true - sass-embedded-android-arm@1.93.3: + sass-embedded-android-arm@1.97.3: optional: true - sass-embedded-android-riscv64@1.93.3: + sass-embedded-android-riscv64@1.97.3: optional: true - sass-embedded-android-x64@1.93.3: + sass-embedded-android-x64@1.97.3: optional: true - sass-embedded-darwin-arm64@1.93.3: + sass-embedded-darwin-arm64@1.97.3: optional: true - sass-embedded-darwin-x64@1.93.3: + sass-embedded-darwin-x64@1.97.3: optional: true - sass-embedded-linux-arm64@1.93.3: + sass-embedded-linux-arm64@1.97.3: optional: true - sass-embedded-linux-arm@1.93.3: + sass-embedded-linux-arm@1.97.3: optional: true - sass-embedded-linux-musl-arm64@1.93.3: + sass-embedded-linux-musl-arm64@1.97.3: optional: true - sass-embedded-linux-musl-arm@1.93.3: + sass-embedded-linux-musl-arm@1.97.3: optional: true - sass-embedded-linux-musl-riscv64@1.93.3: + sass-embedded-linux-musl-riscv64@1.97.3: optional: true - sass-embedded-linux-musl-x64@1.93.3: + sass-embedded-linux-musl-x64@1.97.3: optional: true - sass-embedded-linux-riscv64@1.93.3: + sass-embedded-linux-riscv64@1.97.3: optional: true - sass-embedded-linux-x64@1.93.3: + sass-embedded-linux-x64@1.97.3: optional: true - sass-embedded-unknown-all@1.93.3: + sass-embedded-unknown-all@1.97.3: dependencies: - sass: 1.93.3 + sass: 1.97.3 optional: true - sass-embedded-win32-arm64@1.93.3: + sass-embedded-win32-arm64@1.97.3: optional: true - sass-embedded-win32-x64@1.93.3: + sass-embedded-win32-x64@1.97.3: optional: true - sass-embedded@1.93.3: + sass-embedded@1.97.3: dependencies: '@bufbuild/protobuf': 2.10.1 - buffer-builder: 0.2.0 colorjs.io: 0.5.2 immutable: 5.1.3 rxjs: 7.8.2 @@ -12858,26 +12891,26 @@ snapshots: sync-child-process: 1.0.2 varint: 6.0.0 optionalDependencies: - sass-embedded-all-unknown: 1.93.3 - sass-embedded-android-arm: 1.93.3 - sass-embedded-android-arm64: 1.93.3 - sass-embedded-android-riscv64: 1.93.3 - sass-embedded-android-x64: 1.93.3 - sass-embedded-darwin-arm64: 1.93.3 - sass-embedded-darwin-x64: 1.93.3 - sass-embedded-linux-arm: 1.93.3 - sass-embedded-linux-arm64: 1.93.3 - sass-embedded-linux-musl-arm: 1.93.3 - sass-embedded-linux-musl-arm64: 1.93.3 - sass-embedded-linux-musl-riscv64: 1.93.3 - sass-embedded-linux-musl-x64: 1.93.3 - sass-embedded-linux-riscv64: 1.93.3 - sass-embedded-linux-x64: 1.93.3 - sass-embedded-unknown-all: 1.93.3 - sass-embedded-win32-arm64: 1.93.3 - sass-embedded-win32-x64: 1.93.3 + sass-embedded-all-unknown: 1.97.3 + sass-embedded-android-arm: 1.97.3 + sass-embedded-android-arm64: 1.97.3 + sass-embedded-android-riscv64: 1.97.3 + sass-embedded-android-x64: 1.97.3 + sass-embedded-darwin-arm64: 1.97.3 + sass-embedded-darwin-x64: 1.97.3 + sass-embedded-linux-arm: 1.97.3 + sass-embedded-linux-arm64: 1.97.3 + sass-embedded-linux-musl-arm: 1.97.3 + sass-embedded-linux-musl-arm64: 1.97.3 + sass-embedded-linux-musl-riscv64: 1.97.3 + sass-embedded-linux-musl-x64: 1.97.3 + sass-embedded-linux-riscv64: 1.97.3 + sass-embedded-linux-x64: 1.97.3 + sass-embedded-unknown-all: 1.97.3 + sass-embedded-win32-arm64: 1.97.3 + sass-embedded-win32-x64: 1.97.3 - sass@1.93.3: + sass@1.97.3: dependencies: chokidar: 4.0.3 immutable: 5.1.3 @@ -12895,9 +12928,9 @@ snapshots: schema-utils@4.3.2: dependencies: '@types/json-schema': 7.0.15 - ajv: 8.17.1 - ajv-formats: 2.1.1(ajv@8.17.1) - ajv-keywords: 5.1.0(ajv@8.17.1) + ajv: 8.18.0 + ajv-formats: 2.1.1(ajv@8.18.0) + ajv-keywords: 5.1.0(ajv@8.18.0) semver@6.3.1: {} @@ -13134,33 +13167,33 @@ snapshots: postcss: 8.5.6 postcss-selector-parser: 6.1.2 - stylelint-config-recommended-scss@14.1.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.7.3)): + stylelint-config-recommended-scss@14.1.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.9.3)): dependencies: postcss-scss: 4.0.9(postcss@8.5.6) - stylelint: 16.23.0(typescript@5.7.3) - stylelint-config-recommended: 14.0.1(stylelint@16.23.0(typescript@5.7.3)) - stylelint-scss: 6.12.1(stylelint@16.23.0(typescript@5.7.3)) + stylelint: 16.23.0(typescript@5.9.3) + stylelint-config-recommended: 14.0.1(stylelint@16.23.0(typescript@5.9.3)) + stylelint-scss: 6.12.1(stylelint@16.23.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-recommended@14.0.1(stylelint@16.23.0(typescript@5.7.3)): + stylelint-config-recommended@14.0.1(stylelint@16.23.0(typescript@5.9.3)): dependencies: - stylelint: 16.23.0(typescript@5.7.3) + stylelint: 16.23.0(typescript@5.9.3) - stylelint-config-standard-scss@12.0.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.7.3)): + stylelint-config-standard-scss@12.0.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.9.3)): dependencies: - stylelint: 16.23.0(typescript@5.7.3) - stylelint-config-recommended-scss: 14.1.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.7.3)) - stylelint-config-standard: 35.0.0(stylelint@16.23.0(typescript@5.7.3)) + stylelint: 16.23.0(typescript@5.9.3) + stylelint-config-recommended-scss: 14.1.0(postcss@8.5.6)(stylelint@16.23.0(typescript@5.9.3)) + stylelint-config-standard: 35.0.0(stylelint@16.23.0(typescript@5.9.3)) optionalDependencies: postcss: 8.5.6 - stylelint-config-standard@35.0.0(stylelint@16.23.0(typescript@5.7.3)): + stylelint-config-standard@35.0.0(stylelint@16.23.0(typescript@5.9.3)): dependencies: - stylelint: 16.23.0(typescript@5.7.3) - stylelint-config-recommended: 14.0.1(stylelint@16.23.0(typescript@5.7.3)) + stylelint: 16.23.0(typescript@5.9.3) + stylelint-config-recommended: 14.0.1(stylelint@16.23.0(typescript@5.9.3)) - stylelint-scss@6.12.1(stylelint@16.23.0(typescript@5.7.3)): + stylelint-scss@6.12.1(stylelint@16.23.0(typescript@5.9.3)): dependencies: css-tree: 3.1.0 is-plain-object: 5.0.0 @@ -13170,9 +13203,9 @@ snapshots: postcss-resolve-nested-selector: 0.1.6 postcss-selector-parser: 7.1.0 postcss-value-parser: 4.2.0 - stylelint: 16.23.0(typescript@5.7.3) + stylelint: 16.23.0(typescript@5.9.3) - stylelint@16.23.0(typescript@5.7.3): + stylelint@16.23.0(typescript@5.9.3): dependencies: '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 @@ -13181,7 +13214,7 @@ snapshots: '@dual-bundle/import-meta-resolve': 4.1.0 balanced-match: 2.0.0 colord: 2.9.3 - cosmiconfig: 9.0.0(typescript@5.7.3) + cosmiconfig: 9.0.0(typescript@5.9.3) css-functions-list: 3.2.3 css-tree: 3.1.0 debug: 4.4.1 @@ -13218,7 +13251,7 @@ snapshots: sucrase@3.35.0: dependencies: - '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/gen-mapping': 0.3.13 commander: 4.1.1 glob: 10.4.5 lines-and-columns: 1.2.4 @@ -13275,7 +13308,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@3.4.17: + tailwindcss@3.4.19: dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -13297,7 +13330,7 @@ snapshots: postcss-load-config: 4.0.2(postcss@8.5.6) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 - resolve: 1.22.10 + resolve: 1.22.11 sucrase: 3.35.0 transitivePeerDependencies: - ts-node @@ -13394,10 +13427,6 @@ snapshots: dependencies: typescript: 5.9.2 - ts-api-utils@2.1.0(typescript@5.7.3): - dependencies: - typescript: 5.7.3 - ts-interface-checker@0.1.13: {} tsconfig-paths@3.15.0: @@ -13470,12 +13499,12 @@ snapshots: typescript: 5.9.2 yaml: 2.8.0 - typescript@5.7.3: {} - typescript@5.8.2: {} typescript@5.9.2: {} + typescript@5.9.3: {} + uc.micro@2.1.0: {} ufo@1.6.1: {} @@ -13491,11 +13520,9 @@ snapshots: undici-types@7.16.0: {} - unicode-canonical-property-names-ecmascript@2.0.1: {} + undici@7.22.0: {} - unicode-emoji-utils@1.3.1: - dependencies: - emoji-regex-xs: 2.0.1 + unicode-canonical-property-names-ecmascript@2.0.1: {} unicode-match-property-ecmascript@2.0.0: dependencies: @@ -13579,17 +13606,17 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 - valibot@1.2.0(typescript@5.7.3): - optionalDependencies: - typescript: 5.7.3 - valibot@1.2.0(typescript@5.9.2): optionalDependencies: typescript: 5.9.2 + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + varint@6.0.0: {} - vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.47.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.7.3))(typescript@5.7.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@babel/code-frame': 7.29.0 chokidar: 4.0.3 @@ -13598,17 +13625,17 @@ snapshots: picomatch: 4.0.3 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) vscode-uri: 3.1.0 optionalDependencies: eslint: 8.57.1 meow: 13.2.0 optionator: 0.9.4 - oxlint: 1.47.0(oxlint-tsgolint@0.14.2) - stylelint: 16.23.0(typescript@5.7.3) - typescript: 5.7.3 + oxlint: 1.50.0(oxlint-tsgolint@0.14.2) + stylelint: 16.23.0(typescript@5.9.3) + typescript: 5.9.3 - vite-plugin-compile-time@0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-compile-time@0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 @@ -13619,11 +13646,11 @@ snapshots: devalue: 5.1.1 esbuild: 0.24.2 magic-string: 0.30.17 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) transitivePeerDependencies: - supports-color - vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@20.19.9) '@rollup/pluginutils': 5.2.0(rollup@4.57.1) @@ -13636,13 +13663,13 @@ snapshots: magic-string: 0.30.17 typescript: 5.9.2 optionalDependencies: - vite: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@22.17.0) '@rollup/pluginutils': 5.2.0(rollup@4.57.1) @@ -13655,13 +13682,13 @@ snapshots: magic-string: 0.30.17 typescript: 5.9.2 optionalDependencies: - vite: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-html@3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-html@3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@rollup/pluginutils': 4.2.1 colorette: 2.0.20 @@ -13675,27 +13702,27 @@ snapshots: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) - vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): + vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite-plugin-require@1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-require@1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 '@babel/traverse': 7.28.0 '@babel/types': 7.28.2 '@vue/compiler-sfc': 3.5.18 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) vue-loader: 17.4.2(@vue/compiler-sfc@3.5.18)(webpack@5.101.0(esbuild@0.24.2)) webpack: 5.101.0(esbuild@0.24.2) transitivePeerDependencies: @@ -13706,15 +13733,15 @@ snapshots: - vue - webpack-cli - vite-plugin-static-copy@3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-static-copy@3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) - vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0): + vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13727,12 +13754,12 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 lightningcss: 1.31.1 - sass: 1.93.3 - sass-embedded: 1.93.3 + sass: 1.97.3 + sass-embedded: 1.97.3 terser: 5.46.0 yaml: 2.8.0 - vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0): + vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13745,12 +13772,12 @@ snapshots: fsevents: 2.3.3 jiti: 1.21.7 lightningcss: 1.31.1 - sass: 1.93.3 - sass-embedded: 1.93.3 + sass: 1.97.3 + sass-embedded: 1.97.3 terser: 5.46.0 yaml: 2.8.0 - vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.93.3)(sass@1.93.3)(terser@5.46.0)(yaml@2.8.0): + vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): dependencies: '@oxc-project/runtime': 0.113.0 fdir: 6.5.0(picomatch@4.0.3) @@ -13764,8 +13791,8 @@ snapshots: esbuild: 0.27.3 fsevents: 2.3.3 jiti: 1.21.7 - sass: 1.93.3 - sass-embedded: 1.93.3 + sass: 1.97.3 + sass-embedded: 1.97.3 terser: 5.46.0 yaml: 2.8.0 @@ -13831,12 +13858,15 @@ snapshots: - esbuild - uglify-js - whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} - whatwg-url@15.1.0: + whatwg-url@16.0.1: dependencies: + '@exodus/bytes': 1.14.1 tr46: 6.0.0 webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' whatwg-url@7.1.0: dependencies: @@ -14035,8 +14065,6 @@ snapshots: ws@8.18.3: {} - ws@8.19.0: {} - xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} From d91cbcfd3fa3224293bc2c065154b657024d5d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:15:20 +0100 Subject: [PATCH 047/264] pl-api: update deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/package.json | 22 +- pnpm-lock.yaml | 743 ++++++++--------------------------- 2 files changed, 173 insertions(+), 592 deletions(-) diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index aa0383ccb..4ada82496 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -33,25 +33,25 @@ "lodash.omit": "^4.5.0", "lodash.pick": "^4.4.0", "object-to-formdata": "^4.5.1", - "query-string": "^9.2.2", - "semver": "^7.7.2", + "query-string": "^9.3.1", + "semver": "^7.7.4", "valibot": "^1.2.0" }, "devDependencies": { "@types/http-link-header": "^1.0.7", "@types/lodash.omit": "^4.5.9", "@types/lodash.pick": "^4.4.9", - "@types/node": "^22.13.11", - "@types/semver": "^7.5.8", - "oxfmt": "^0.32.0", - "oxlint": "^1.47.0", - "typedoc": "^0.28.7", - "typedoc-material-theme": "^1.4.0", + "@types/node": "^25.3.0", + "@types/semver": "^7.7.1", + "oxfmt": "^0.35.0", + "oxlint": "^1.50.0", + "typedoc": "^0.28.17", + "typedoc-material-theme": "^1.4.1", "typedoc-plugin-valibot": "^1.0.0", - "typescript": "^5.8.3", - "vite": "^7.0.0", + "typescript": "^5.9.3", + "vite": "^7.3.1", "vite-plugin-dts": "^4.5.4", - "ws": "^8.18.3" + "ws": "^8.19.0" }, "lint-staged": { "*.{js,cjs,mjs,ts}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 707f591ef..338b2dfeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,7 +32,7 @@ importers: version: 1.1.3 isows: specifier: ^1.0.7 - version: 1.0.7(ws@8.18.3) + version: 1.0.7(ws@8.19.0) lodash.omit: specifier: ^4.5.0 version: 4.5.0 @@ -43,14 +43,14 @@ importers: specifier: ^4.5.1 version: 4.5.1 query-string: - specifier: ^9.2.2 - version: 9.2.2 + specifier: ^9.3.1 + version: 9.3.1 semver: - specifier: ^7.7.2 - version: 7.7.2 + specifier: ^7.7.4 + version: 7.7.4 valibot: specifier: ^1.2.0 - version: 1.2.0(typescript@5.9.2) + version: 1.2.0(typescript@5.9.3) devDependencies: '@types/http-link-header': specifier: ^1.0.7 @@ -62,38 +62,38 @@ importers: specifier: ^4.4.9 version: 4.4.9 '@types/node': - specifier: ^22.13.11 - version: 22.17.0 + specifier: ^25.3.0 + version: 25.3.0 '@types/semver': - specifier: ^7.5.8 - version: 7.7.0 + specifier: ^7.7.1 + version: 7.7.1 oxfmt: - specifier: ^0.32.0 - version: 0.32.0 + specifier: ^0.35.0 + version: 0.35.0 oxlint: - specifier: ^1.47.0 - version: 1.47.0(oxlint-tsgolint@0.14.2) + specifier: ^1.50.0 + version: 1.50.0(oxlint-tsgolint@0.14.2) typedoc: - specifier: ^0.28.7 - version: 0.28.9(typescript@5.9.2) + specifier: ^0.28.17 + version: 0.28.17(typescript@5.9.3) typedoc-material-theme: - specifier: ^1.4.0 - version: 1.4.0(typedoc@0.28.9(typescript@5.9.2)) + specifier: ^1.4.1 + version: 1.4.1(typedoc@0.28.17(typescript@5.9.3)) typedoc-plugin-valibot: specifier: ^1.0.0 - version: 1.0.1(typedoc@0.28.9(typescript@5.9.2)) + version: 1.0.1(typedoc@0.28.17(typescript@5.9.3)) typescript: - specifier: ^5.8.3 - version: 5.9.2 + specifier: ^5.9.3 + version: 5.9.3 vite: - specifier: ^7.0.0 - version: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vite-plugin-dts: specifier: ^4.5.4 - version: 4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.5.4(@types/node@25.3.0)(rollup@4.57.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) ws: - specifier: ^8.18.3 - version: 8.18.3 + specifier: ^8.19.0 + version: 8.19.0 packages/pl-fe: dependencies: @@ -430,7 +430,7 @@ importers: version: 0.13.6 '@vitejs/plugin-react': specifier: ^5.1.3 - version: 5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) eslint-plugin-formatjs: specifier: ^6.2.0 version: 6.2.0(eslint@8.57.1) @@ -448,7 +448,7 @@ importers: version: 0.14.2 rollup-plugin-bundle-stats: specifier: ^4.21.10 - version: 4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) stylelint: specifier: ^16.12.0 version: 16.23.0(typescript@5.9.3) @@ -469,25 +469,25 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + version: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vite-plugin-checker: specifier: ^0.12.0 - version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) vite-plugin-compile-time: specifier: ^0.4.6 - version: 0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 0.4.6(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) vite-plugin-html: specifier: ^3.2.2 - version: 3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 3.2.2(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) vite-plugin-pwa: specifier: ^1.2.0 - version: 1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) + version: 1.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0) vite-plugin-require: specifier: ^1.2.14 - version: 1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) vite-plugin-static-copy: specifier: ^3.2.0 - version: 3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 3.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) packages/pl-hooks: dependencies: @@ -542,10 +542,10 @@ importers: version: 5.9.2 vite: specifier: ^8.0.0-beta.14 - version: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + version: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vite-plugin-dts: specifier: ^4.2.1 - version: 4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + version: 4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) packages: @@ -1659,8 +1659,8 @@ packages: ts-jest: optional: true - '@gerrit0/mini-shiki@3.9.1': - resolution: {integrity: sha512-quvtbDhNf528BkMHQQd8xGJMpmA5taDZuex/JDF8ETEjS2iypXzr1hnEUVh+lTUyffFJ0JCxysUsiuUoEGIz/Q==} + '@gerrit0/mini-shiki@3.22.0': + resolution: {integrity: sha512-jMpciqEVUBKE1QwU64S4saNMzpsSza6diNCk4MWAeCxO2+LFi2FIFmL2S0VDLzEJCxuvCbU783xi8Hp/gkM5CQ==} '@humanwhocodes/config-array@0.13.0': resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} @@ -1851,228 +1851,114 @@ packages: '@oxc-project/types@0.113.0': resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==} - '@oxfmt/binding-android-arm-eabi@0.32.0': - resolution: {integrity: sha512-DpVyuVzgLH6/MvuB/YD3vXO9CN/o9EdRpA0zXwe/tagP6yfVSFkFWkPqTROdqp0mlzLH5Yl+/m+hOrcM601EbA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - '@oxfmt/binding-android-arm-eabi@0.35.0': resolution: {integrity: sha512-BaRKlM3DyG81y/xWTsE6gZiv89F/3pHe2BqX2H4JbiB8HNVlWWtplzgATAE5IDSdwChdeuWLDTQzJ92Lglw3ZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.32.0': - resolution: {integrity: sha512-w1cmNXf9zs0vKLuNgyUF3hZ9VUAS1hBmQGndYJv1OmcVqStBtRTRNxSWkWM0TMkrA9UbvIvM9gfN+ib4Wy6lkQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@oxfmt/binding-android-arm64@0.35.0': resolution: {integrity: sha512-/O+EbuAJYs6nde/anv+aID6uHsGQApyE9JtYBo/79KyU8e6RBN3DMbT0ix97y1SOnCglurmL2iZ+hlohjP2PnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.32.0': - resolution: {integrity: sha512-m6wQojz/hn94XdZugFPtdFbOvXbOSYEqPsR2gyLyID3BvcrC2QsJyT1o3gb4BZEGtZrG1NiKVGwDRLM0dHd2mg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@oxfmt/binding-darwin-arm64@0.35.0': resolution: {integrity: sha512-pGqRtqlNdn9d4VrmGUWVyQjkw79ryhI6je9y2jfqNUIZCfqceob+R97YYAoG7C5TFyt8ILdLVoN+L2vw/hSFyA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.32.0': - resolution: {integrity: sha512-hN966Uh6r3Erkg2MvRcrJWaB6QpBzP15rxWK/QtkUyD47eItJLsAQ2Hrm88zMIpFZ3COXZLuN3hqgSlUtvB0Xw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@oxfmt/binding-darwin-x64@0.35.0': resolution: {integrity: sha512-8GmsDcSozTPjrCJeGpp+sCmS9+9V5yRrdEZ1p/sTWxPG5nYeAfSLuS0nuEYjXSO+CtdSbStIW6dxa+4NM58yRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.32.0': - resolution: {integrity: sha512-g5UZPGt8tJj263OfSiDGdS54HPa0KgFfspLVAUivVSdoOgsk6DkwVS9nO16xQTDztzBPGxTvrby8WuufF0g86Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@oxfmt/binding-freebsd-x64@0.35.0': resolution: {integrity: sha512-QyfKfTe0ytHpFKHAcHCGQEzN45QSqq1AHJOYYxQMgLM3KY4xu8OsXHpCnINjDsV4XGnQzczJDU9e04Zmd8XqIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': - resolution: {integrity: sha512-F4ZY83/PVQo9ZJhtzoMqbmjqEyTVEZjbaw4x1RhzdfUhddB41ZB2Vrt4eZi7b4a4TP85gjPRHgQBeO0c1jbtaw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': resolution: {integrity: sha512-u+kv3JD6P3J38oOyUaiCqgY5TNESzBRZJ5lyZQ6c2czUW2v5SIN9E/KWWa9vxoc+P8AFXQFUVrdzGy1tK+nbPQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.32.0': - resolution: {integrity: sha512-olR37eG16Lzdj9OBSvuoT5RxzgM5xfQEHm1OEjB3M7Wm4KWa5TDWIT13Aiy74GvAN77Hq1+kUKcGVJ/0ynf75g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.35.0': resolution: {integrity: sha512-1NiZroCiV57I7Pf8kOH4XGR366kW5zir3VfSMBU2D0V14GpYjiYmPYFAoJboZvp8ACnZKUReWyMkNKSa5ad58A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.32.0': - resolution: {integrity: sha512-eZhk6AIjRCDeLoXYBhMW7qq/R1YyVi+tGnGfc3kp7AZQrMsFaWtP/bgdCJCTNXMpbMwymtVz0qhSQvR5w2sKcg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.35.0': resolution: {integrity: sha512-7Q0Xeg7ZnW2nxnZ4R7aF6DEbCFls4skgHZg+I63XitpNvJCbVIU8MFOTZlvZGRsY9+rPgWPQGeUpLHlyx0wvMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxfmt/binding-linux-arm64-musl@0.32.0': - resolution: {integrity: sha512-UYiqO9MlipntFbdbUKOIo84vuyzrK4TVIs7Etat91WNMFSW54F6OnHq08xa5ZM+K9+cyYMgQPXvYCopuP+LyKw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@oxfmt/binding-linux-arm64-musl@0.35.0': resolution: {integrity: sha512-5Okqi+uhYFxwKz8hcnUftNNwdm8BCkf6GSCbcz9xJxYMm87k1E4p7PEmAAbhLTk7cjSdDre6TDL0pDzNX+Y22Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxfmt/binding-linux-ppc64-gnu@0.32.0': - resolution: {integrity: sha512-IDH/fxMv+HmKsMtsjEbXqhScCKDIYp38sgGEcn0QKeXMxrda67PPZA7HMfoUwEtFUG+jsO1XJxTrQsL+kQ90xQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - '@oxfmt/binding-linux-ppc64-gnu@0.35.0': resolution: {integrity: sha512-9k66pbZQXM/lBJWys3Xbc5yhl4JexyfqkEf/tvtq8976VIJnLAAL3M127xHA3ifYSqxdVHfVGTg84eiBHCGcNw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxfmt/binding-linux-riscv64-gnu@0.32.0': - resolution: {integrity: sha512-bQFGPDa0buYWJFeK2I7ah8wRZjrAgamaG2OAGv+Ua5UMYEnHxmHcv+r8lWUUrwP2oqQGvp1SB8JIVtBbYuAueQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - '@oxfmt/binding-linux-riscv64-gnu@0.35.0': resolution: {integrity: sha512-aUcY9ofKPtjO52idT6t0SAQvEF6ctjzUQa1lLp7GDsRpSBvuTrBQGeq0rYKz3gN8dMIQ7mtMdGD9tT4LhR8jAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxfmt/binding-linux-riscv64-musl@0.32.0': - resolution: {integrity: sha512-3vFp9DW1ItEKWltADzCFqG5N7rYFToT4ztlhg8wALoo2E2VhveLD88uAF4FF9AxD9NhgHDGmPCV+WZl/Qlj8cQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - '@oxfmt/binding-linux-riscv64-musl@0.35.0': resolution: {integrity: sha512-C6yhY5Hvc2sGM+mCPek9ZLe5xRUOC/BvhAt2qIWFAeXMn4il04EYIjl3DsWiJr0xDMTJhvMOmD55xTRPlNp39w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxfmt/binding-linux-s390x-gnu@0.32.0': - resolution: {integrity: sha512-Fub2y8S9ImuPzAzpbgkoz/EVTWFFBolxFZYCMRhRZc8cJZI2gl/NlZswqhvJd/U0Jopnwgm/OJ2x128vVzFFWA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - '@oxfmt/binding-linux-s390x-gnu@0.35.0': resolution: {integrity: sha512-RG2hlvOMK4OMZpO3mt8MpxLQ0AAezlFqhn5mI/g5YrVbPFyoCv9a34AAvbSJS501ocOxlFIRcKEuw5hFvddf9g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxfmt/binding-linux-x64-gnu@0.32.0': - resolution: {integrity: sha512-XufwsnV3BF81zO2ofZvhT4FFaMmLTzZEZnC9HpFz/quPeg9C948+kbLlZnsfjmp+1dUxKMCpfmRMqOfF4AOLsA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@oxfmt/binding-linux-x64-gnu@0.35.0': resolution: {integrity: sha512-wzmh90Pwvqj9xOKHJjkQYBpydRkaXG77ZvDz+iFDRRQpnqIEqGm5gmim2s6vnZIkDGsvKCuTdtxm0GFmBjM1+w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxfmt/binding-linux-x64-musl@0.32.0': - resolution: {integrity: sha512-u2f9tC2qYfikKmA2uGpnEJgManwmk0ZXWs5BB4ga4KDu2JNLdA3i634DGHeMLK9wY9+iRf3t7IYpgN3OVFrvDw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@oxfmt/binding-linux-x64-musl@0.35.0': resolution: {integrity: sha512-+HCqYCJPCUy5I+b2cf+gUVaApfgtoQT3HdnSg/l7NIcLHOhKstlYaGyrFZLmUpQt4WkFbpGKZZayG6zjRU0KFA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxfmt/binding-openharmony-arm64@0.32.0': - resolution: {integrity: sha512-5ZXb1wrdbZ1YFXuNXNUCePLlmLDy4sUt4evvzD4Cgumbup5wJgS9PIe5BOaLywUg9f1wTH6lwltj3oT7dFpIGA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@oxfmt/binding-openharmony-arm64@0.35.0': resolution: {integrity: sha512-kFYmWfR9YL78XyO5ws+1dsxNvZoD973qfVMNFOS4e9bcHXGF7DvGC2tY5UDFwyMCeB33t3sDIuGONKggnVNSJA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.32.0': - resolution: {integrity: sha512-IGSMm/Agq+IA0++aeAV/AGPfjcBdjrsajB5YpM3j7cMcwoYgUTi/k2YwAmsHH3ueZUE98pSM/Ise2J7HtyRjOA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@oxfmt/binding-win32-arm64-msvc@0.35.0': resolution: {integrity: sha512-uD/NGdM65eKNCDGyTGdO8e9n3IHX+wwuorBvEYrPJXhDXL9qz6gzddmXH8EN04ejUXUujlq4FsoSeCfbg0Y+Jg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.32.0': - resolution: {integrity: sha512-H/9gsuqXmceWMsVoCPZhtJG2jLbnBeKr7xAXm2zuKpxLVF7/2n0eh7ocOLB6t+L1ARE76iORuUsRMnuGjj8FjQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.35.0': resolution: {integrity: sha512-oSRD2k8J2uxYDEKR2nAE/YTY9PobOEnhZgCmspHu0+yBQ665yH8lFErQVSTE7fcGJmJp/cC6322/gc8VFuQf7g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.32.0': - resolution: {integrity: sha512-fF8VIOeligq+mA6KfKvWtFRXbf0EFy73TdR6ZnNejdJRM8VWN1e3QFhYgIwD7O8jBrQsd7EJbUpkAr/YlUOokg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.35.0': resolution: {integrity: sha512-WCDJjlS95NboR0ugI2BEwzt1tYvRDorDRM9Lvctls1SLyKYuNRCyrPwp1urUPFBnwgBNn9p2/gnmo7gFMySRoQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2109,228 +1995,114 @@ packages: cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.47.0': - resolution: {integrity: sha512-UHqo3te9K/fh29brCuQdHjN+kfpIi9cnTPABuD5S9wb9ykXYRGTOOMVuSV/CK43sOhU4wwb2nT1RVjcbrrQjFw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - '@oxlint/binding-android-arm-eabi@1.50.0': resolution: {integrity: sha512-G7MRGk/6NCe+L8ntonRdZP7IkBfEpiZ/he3buLK6JkLgMHgJShXZ+BeOwADmspXez7U7F7L1Anf4xLSkLHiGTg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.47.0': - resolution: {integrity: sha512-xh02lsTF1TAkR+SZrRMYHR/xCx8Wg2MAHxJNdHVpAKELh9/yE9h4LJeqAOBbIb3YYn8o/D97U9VmkvkfJfrHfw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - '@oxlint/binding-android-arm64@1.50.0': resolution: {integrity: sha512-GeSuMoJWCVpovJi/e3xDSNgjeR8WEZ6MCXL6EtPiCIM2NTzv7LbflARINTXTJy2oFBYyvdf/l2PwHzYo6EdXvg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.47.0': - resolution: {integrity: sha512-OSOfNJqabOYbkyQDGT5pdoL+05qgyrmlQrvtCO58M4iKGEQ/xf3XkkKj7ws+hO+k8Y4VF4zGlBsJlwqy7qBcHA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - '@oxlint/binding-darwin-arm64@1.50.0': resolution: {integrity: sha512-w3SY5YtxGnxCHPJ8Twl3KmS9oja1gERYk3AMoZ7Hv8P43ZtB6HVfs02TxvarxfL214Tm3uzvc2vn+DhtUNeKnw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.47.0': - resolution: {integrity: sha512-hP2bOI4IWNS+F6pVXWtRshSTuJ1qCRZgDgVUg6EBUqsRy+ExkEPJkx+YmIuxgdCduYK1LKptLNFuQLJP8voPbQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - '@oxlint/binding-darwin-x64@1.50.0': resolution: {integrity: sha512-hNfogDqy7tvmllXKBSlHo6k5x7dhTUVOHbMSE15CCAcXzmqf5883aPvBYPOq9AE7DpDUQUZ1kVE22YbiGW+tuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.47.0': - resolution: {integrity: sha512-F55jIEH5xmGu7S661Uho8vGiLFk0bY3A/g4J8CTKiLJnYu/PSMZ2WxFoy5Hji6qvFuujrrM9Q8XXbMO0fKOYPg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - '@oxlint/binding-freebsd-x64@1.50.0': resolution: {integrity: sha512-ykZevOWEyu0nsxolA911ucxpEv0ahw8jfEeGWOwwb/VPoE4xoexuTOAiPNlWZNJqANlJl7yp8OyzCtXTUAxotw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.47.0': - resolution: {integrity: sha512-wxmOn/wns/WKPXUC1fo5mu9pMZPVOu8hsynaVDrgmmXMdHKS7on6bA5cPauFFN9tJXNdsjW26AK9lpfu3IfHBQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxlint/binding-linux-arm-gnueabihf@1.50.0': resolution: {integrity: sha512-hif3iDk7vo5GGJ4OLCCZAf2vjnU9FztGw4L0MbQL0M2iY9LKFtDMMiQAHmkF0PQGQMVbTYtPdXCLKVgdkiqWXQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.47.0': - resolution: {integrity: sha512-KJTmVIA/GqRlM2K+ZROH30VMdydEU7bDTY35fNg3tOPzQRIs2deLZlY/9JWwdWo1F/9mIYmpbdCmPqtKhWNOPg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.50.0': resolution: {integrity: sha512-dVp9iSssiGAnTNey2Ruf6xUaQhdnvcFOJyRWd/mu5o2jVbFK15E5fbWGeFRfmuobu5QXuROtFga44+7DOS3PLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.47.0': - resolution: {integrity: sha512-PF7ELcFg1GVlS0X0ZB6aWiXobjLrAKer3T8YEkwIoO8RwWiAMkL3n3gbleg895BuZkHVlJ2kPRUwfrhHrVkD1A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.50.0': resolution: {integrity: sha512-1cT7yz2HA910CKA9NkH1ZJo50vTtmND2fkoW1oyiSb0j6WvNtJ0Wx2zoySfXWc/c+7HFoqRK5AbEoL41LOn9oA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-arm64-musl@1.47.0': - resolution: {integrity: sha512-4BezLRO5cu0asf0Jp1gkrnn2OHiXrPPPEfBTxq1k5/yJ2zdGGTmZxHD2KF2voR23wb8Elyu3iQawXo7wvIZq0Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - '@oxlint/binding-linux-arm64-musl@1.50.0': resolution: {integrity: sha512-++B3k/HEPFVlj89cOz8kWfQccMZB/aWL9AhsW7jPIkG++63Mpwb2cE9XOEsd0PATbIan78k2Gky+09uWM1d/gQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] - '@oxlint/binding-linux-ppc64-gnu@1.47.0': - resolution: {integrity: sha512-aI5ds9jq2CPDOvjeapiIj48T/vlWp+f4prkxs+FVzrmVN9BWIj0eqeJ/hV8WgXg79HVMIz9PU6deI2ki09bR1w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - '@oxlint/binding-linux-ppc64-gnu@1.50.0': resolution: {integrity: sha512-Z9b/KpFMkx66w3gVBqjIC1AJBTZAGoI9+U+K5L4QM0CB/G0JSNC1es9b3Y0Vcrlvtdn8A+IQTkYjd/Q0uCSaZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] - '@oxlint/binding-linux-riscv64-gnu@1.47.0': - resolution: {integrity: sha512-mO7ycp9Elvgt5EdGkQHCwJA6878xvo9tk+vlMfT1qg++UjvOMB8INsOCQIOH2IKErF/8/P21LULkdIrocMw9xA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - '@oxlint/binding-linux-riscv64-gnu@1.50.0': resolution: {integrity: sha512-jvmuIw8wRSohsQlFNIST5uUwkEtEJmOQYr33bf/K2FrFPXHhM4KqGekI3ShYJemFS/gARVacQFgBzzJKCAyJjg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-riscv64-musl@1.47.0': - resolution: {integrity: sha512-24D0wsYT/7hDFn3Ow32m3/+QT/1ZwrUhShx4/wRDAmz11GQHOZ1k+/HBuK/MflebdnalmXWITcPEy4BWTi7TCA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - '@oxlint/binding-linux-riscv64-musl@1.50.0': resolution: {integrity: sha512-x+UrN47oYNh90nmAAyql8eQaaRpHbDPu5guasDg10+OpszUQ3/1+1J6zFMmV4xfIEgTcUXG/oI5fxJhF4eWCNA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] - '@oxlint/binding-linux-s390x-gnu@1.47.0': - resolution: {integrity: sha512-8tPzPne882mtML/uy3mApvdCyuVOpthJ7xUv3b67gVfz63hOOM/bwO0cysSkPyYYFDFRn6/FnUb7Jhmsesntvg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - '@oxlint/binding-linux-s390x-gnu@1.50.0': resolution: {integrity: sha512-i/JLi2ljLUIVfekMj4ISmdt+Hn11wzYUdRRrkVUYsCWw7zAy5xV7X9iA+KMyM156LTFympa7s3oKBjuCLoTAUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - '@oxlint/binding-linux-x64-gnu@1.47.0': - resolution: {integrity: sha512-q58pIyGIzeffEBhEgbRxLFHmHfV9m7g1RnkLiahQuEvyjKNiJcvdHOwKH2BdgZxdzc99Cs6hF5xTa86X40WzPw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@oxlint/binding-linux-x64-gnu@1.50.0': resolution: {integrity: sha512-/C7brhn6c6UUPccgSPCcpLQXcp+xKIW/3sji/5VZ8/OItL3tQ2U7KalHz887UxxSQeEOmd1kY6lrpuwFnmNqOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-linux-x64-musl@1.47.0': - resolution: {integrity: sha512-e7DiLZtETZUCwTa4EEHg9G+7g3pY+afCWXvSeMG7m0TQ29UHHxMARPaEQUE4mfKgSqIWnJaUk2iZzRPMRdga5g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - '@oxlint/binding-linux-x64-musl@1.50.0': resolution: {integrity: sha512-oDR1f+bGOYU8LfgtEW8XtotWGB63ghtcxk5Jm6IDTCk++rTA/IRMsjOid2iMd+1bW+nP9Mdsmcdc7VbPD3+iyQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] - '@oxlint/binding-openharmony-arm64@1.47.0': - resolution: {integrity: sha512-3AFPfQ0WKMleT/bKd7zsks3xoawtZA6E/wKf0DjwysH7wUiMMJkNKXOzYq1R/00G98JFgSU1AkrlOQrSdNNhlg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - '@oxlint/binding-openharmony-arm64@1.50.0': resolution: {integrity: sha512-4CmRGPp5UpvXyu4jjP9Tey/SrXDQLRvZXm4pb4vdZBxAzbFZkCyh0KyRy4txld/kZKTJlW4TO8N1JKrNEk+mWw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.47.0': - resolution: {integrity: sha512-cLMVVM6TBxp+N7FldQJ2GQnkcLYEPGgiuEaXdvhgvSgODBk9ov3jed+khIXSAWtnFOW0wOnG3RjwqPh0rCuheA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - '@oxlint/binding-win32-arm64-msvc@1.50.0': resolution: {integrity: sha512-Fq0M6vsGcFsSfeuWAACDhd5KJrO85ckbEfe1EGuBj+KPyJz7KeWte2fSFrFGmNKNXyhEMyx4tbgxiWRujBM2KQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.47.0': - resolution: {integrity: sha512-VpFOSzvTnld77/Edje3ZdHgZWnlTb5nVWXyTgjD3/DKF/6t5bRRbwn3z77zOdnGy44xAMvbyAwDNOSeOdVUmRA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.50.0': resolution: {integrity: sha512-qTdWR9KwY/vxJGhHVIZG2eBOhidOQvOwzDxnX+jhW/zIVacal1nAhR8GLkiywW8BIFDkQKXo/zOfT+/DY+ns/w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.47.0': - resolution: {integrity: sha512-+q8IWptxXx2HMTM6JluR67284t0h8X/oHJgqpxH1siowxPMqZeIpAcWCUq+tY+Rv2iQK8TUugjZnSBQAVV5CmA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - '@oxlint/binding-win32-x64-msvc@1.50.0': resolution: {integrity: sha512-682t7npLC4G2Ca+iNlI9fhAKTcFPYYXJjwoa88H4q+u5HHHlsnL/gHULapX3iqp+A8FIJbgdylL5KMYo2LaluQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2852,17 +2624,17 @@ packages: resolution: {integrity: sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog==} engines: {node: '>=14.18'} - '@shikijs/engine-oniguruma@3.9.2': - resolution: {integrity: sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==} + '@shikijs/engine-oniguruma@3.22.0': + resolution: {integrity: sha512-DyXsOG0vGtNtl7ygvabHd7Mt5EY8gCNqR9Y7Lpbbd/PbJvgWrqaKzH1JW6H6qFkuUa8aCxoiYVv8/YfFljiQxA==} - '@shikijs/langs@3.9.2': - resolution: {integrity: sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==} + '@shikijs/langs@3.22.0': + resolution: {integrity: sha512-x/42TfhWmp6H00T6uwVrdTJGKgNdFbrEdhaDwSR5fd5zhQ1Q46bHq9EO61SCEWJR0HY7z2HNDMaBZp8JRmKiIA==} - '@shikijs/themes@3.9.2': - resolution: {integrity: sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==} + '@shikijs/themes@3.22.0': + resolution: {integrity: sha512-o+tlOKqsr6FE4+mYJG08tfCFDS+3CG20HbldXeVoyP+cYSUxDhrFf3GPjE60U55iOkkjbpY2uC3It/eeja35/g==} - '@shikijs/types@3.9.2': - resolution: {integrity: sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==} + '@shikijs/types@3.22.0': + resolution: {integrity: sha512-491iAekgKDBFE67z70Ok5a8KBMsQ2IJwOWw3us/7ffQkIBCyOQfm/aNwVMBUriP02QshIfgHCBSIYAl3u2eWjg==} '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} @@ -3043,14 +2815,11 @@ packages: '@types/node@20.19.9': resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} - '@types/node@22.17.0': - resolution: {integrity: sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==} - '@types/node@22.19.11': resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} - '@types/node@25.0.3': - resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + '@types/node@25.3.0': + resolution: {integrity: sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==} '@types/path-browserify@1.0.3': resolution: {integrity: sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==} @@ -3091,8 +2860,8 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/semver@7.7.0': - resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -5322,11 +5091,6 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} - oxfmt@0.32.0: - resolution: {integrity: sha512-KArQhGzt/Y8M1eSAX98Y8DLtGYYDQhkR55THUPY5VNcpFQ+9nRZkL3ULXhagHMD2hIvjy8JSeEQEP5/yYJSrLA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - oxfmt@0.35.0: resolution: {integrity: sha512-QYeXWkP+aLt7utt5SLivNIk09glWx9QE235ODjgcEZ3sd1VMaUBSpLymh6ZRCA76gD2rMP4bXanUz/fx+nLM9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5336,16 +5100,6 @@ packages: resolution: {integrity: sha512-XJsFIQwnYJgXFlNDz2MncQMWYxwnfy4BCy73mdiFN/P13gEZrAfBU4Jmz2XXFf9UG0wPILdi7hYa6t0KmKQLhw==} hasBin: true - oxlint@1.47.0: - resolution: {integrity: sha512-v7xkK1iv1qdvTxJGclM97QzN8hHs5816AneFAQ0NGji1BMUquhiDAhXpMwp8+ls16uRVJtzVHxP9pAAXblDeGA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - oxlint-tsgolint: '>=0.11.2' - peerDependenciesMeta: - oxlint-tsgolint: - optional: true - oxlint@1.50.0: resolution: {integrity: sha512-iSJ4IZEICBma8cZX7kxIIz9PzsYLF2FaLAYN6RKu7VwRVKdu7RIgpP99bTZaGl//Yao7fsaGZLSEo5xBrI5ReQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5745,10 +5499,6 @@ packages: quansync@0.2.10: resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} - query-string@9.2.2: - resolution: {integrity: sha512-pDSIZJ9sFuOp6VnD+5IkakSVf+rICAuuU88Hcsr6AKL0QtxSIfVuKiVP2oahFI7tk3CRSexwV+Ya6MOoTxzg9g==} - engines: {node: '>=18'} - query-string@9.3.1: resolution: {integrity: sha512-5fBfMOcDi5SA9qj5jZhWAcTtDfKF5WFdd2uD9nVNlbxVv1baq65aALy6qofpNEGELHvisjjasxQp7BlM9gvMzw==} engines: {node: '>=18'} @@ -6200,8 +5950,8 @@ packages: engines: {node: '>=10'} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -6648,8 +6398,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typedoc-material-theme@1.4.0: - resolution: {integrity: sha512-TBoBpX/4zWO6l74/wBLivXHC2rIiD70KXMliYrw1KhcqdybyxkVBLP5z8KiJuNV8aQIeS+rK2QG6GSucQHJQDQ==} + typedoc-material-theme@1.4.1: + resolution: {integrity: sha512-/inKZw8SqZPt+pmawpMhDmXCJQyIm+fHFuGChioyJQICZcX2FyzpwZnyPWcZHmJ09upttWFhti4ZI3hESJNkSA==} engines: {node: '>=18.0.0', npm: '>=8.6.0'} peerDependencies: typedoc: ^0.25.13 || ^0.26.x || ^0.27.x || ^0.28.x @@ -6659,8 +6409,8 @@ packages: peerDependencies: typedoc: 0.23.x || 0.24.x || 0.25.x || 0.26.x || 0.27.x || 0.28.x - typedoc@0.28.9: - resolution: {integrity: sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==} + typedoc@0.28.17: + resolution: {integrity: sha512-ZkJ2G7mZrbxrKxinTQMjFqsCoYY6a5Luwv2GKbTnBCEgV2ihYm5CflA9JnJAwH0pZWavqfYxmDkFHPt4yx2oDQ==} engines: {node: '>= 18', pnpm: '>= 10'} hasBin: true peerDependencies: @@ -6694,8 +6444,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} undici@7.22.0: resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} @@ -7100,8 +6850,8 @@ packages: resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - ws@8.18.3: - resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + ws@8.19.0: + resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -7130,6 +6880,11 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yaml@2.8.2: + resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} + engines: {node: '>= 14.6'} + hasBin: true + yjs@13.6.27: resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -8287,12 +8042,12 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 - '@gerrit0/mini-shiki@3.9.1': + '@gerrit0/mini-shiki@3.22.0': dependencies: - '@shikijs/engine-oniguruma': 3.9.2 - '@shikijs/langs': 3.9.2 - '@shikijs/themes': 3.9.2 - '@shikijs/types': 3.9.2 + '@shikijs/engine-oniguruma': 3.22.0 + '@shikijs/langs': 3.22.0 + '@shikijs/themes': 3.22.0 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 '@humanwhocodes/config-array@0.13.0': @@ -8540,11 +8295,11 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor-model@7.30.7(@types/node@22.17.0)': + '@microsoft/api-extractor-model@7.30.7(@types/node@25.3.0)': dependencies: '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + '@rushstack/node-core-library': 5.14.0(@types/node@25.3.0) transitivePeerDependencies: - '@types/node' @@ -8566,15 +8321,15 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.52.10(@types/node@22.17.0)': + '@microsoft/api-extractor@7.52.10(@types/node@25.3.0)': dependencies: - '@microsoft/api-extractor-model': 7.30.7(@types/node@22.17.0) + '@microsoft/api-extractor-model': 7.30.7(@types/node@25.3.0) '@microsoft/tsdoc': 0.15.1 '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + '@rushstack/node-core-library': 5.14.0(@types/node@25.3.0) '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.4(@types/node@22.17.0) - '@rushstack/ts-command-line': 5.0.2(@types/node@22.17.0) + '@rushstack/terminal': 0.15.4(@types/node@25.3.0) + '@rushstack/ts-command-line': 5.0.2(@types/node@25.3.0) lodash: 4.17.23 minimatch: 10.0.3 resolve: 1.22.10 @@ -8627,117 +8382,60 @@ snapshots: '@oxc-project/types@0.113.0': {} - '@oxfmt/binding-android-arm-eabi@0.32.0': - optional: true - '@oxfmt/binding-android-arm-eabi@0.35.0': optional: true - '@oxfmt/binding-android-arm64@0.32.0': - optional: true - '@oxfmt/binding-android-arm64@0.35.0': optional: true - '@oxfmt/binding-darwin-arm64@0.32.0': - optional: true - '@oxfmt/binding-darwin-arm64@0.35.0': optional: true - '@oxfmt/binding-darwin-x64@0.32.0': - optional: true - '@oxfmt/binding-darwin-x64@0.35.0': optional: true - '@oxfmt/binding-freebsd-x64@0.32.0': - optional: true - '@oxfmt/binding-freebsd-x64@0.35.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.32.0': - optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.35.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.32.0': - optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.35.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.32.0': - optional: true - '@oxfmt/binding-linux-arm64-gnu@0.35.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.32.0': - optional: true - '@oxfmt/binding-linux-arm64-musl@0.35.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.32.0': - optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.35.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.32.0': - optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.35.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.32.0': - optional: true - '@oxfmt/binding-linux-riscv64-musl@0.35.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.32.0': - optional: true - '@oxfmt/binding-linux-s390x-gnu@0.35.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.32.0': - optional: true - '@oxfmt/binding-linux-x64-gnu@0.35.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.32.0': - optional: true - '@oxfmt/binding-linux-x64-musl@0.35.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.32.0': - optional: true - '@oxfmt/binding-openharmony-arm64@0.35.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.32.0': - optional: true - '@oxfmt/binding-win32-arm64-msvc@0.35.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.32.0': - optional: true - '@oxfmt/binding-win32-ia32-msvc@0.35.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.32.0': - optional: true - '@oxfmt/binding-win32-x64-msvc@0.35.0': optional: true @@ -8759,117 +8457,60 @@ snapshots: '@oxlint-tsgolint/win32-x64@0.14.2': optional: true - '@oxlint/binding-android-arm-eabi@1.47.0': - optional: true - '@oxlint/binding-android-arm-eabi@1.50.0': optional: true - '@oxlint/binding-android-arm64@1.47.0': - optional: true - '@oxlint/binding-android-arm64@1.50.0': optional: true - '@oxlint/binding-darwin-arm64@1.47.0': - optional: true - '@oxlint/binding-darwin-arm64@1.50.0': optional: true - '@oxlint/binding-darwin-x64@1.47.0': - optional: true - '@oxlint/binding-darwin-x64@1.50.0': optional: true - '@oxlint/binding-freebsd-x64@1.47.0': - optional: true - '@oxlint/binding-freebsd-x64@1.50.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.47.0': - optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.50.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.47.0': - optional: true - '@oxlint/binding-linux-arm-musleabihf@1.50.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.47.0': - optional: true - '@oxlint/binding-linux-arm64-gnu@1.50.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.47.0': - optional: true - '@oxlint/binding-linux-arm64-musl@1.50.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.47.0': - optional: true - '@oxlint/binding-linux-ppc64-gnu@1.50.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.47.0': - optional: true - '@oxlint/binding-linux-riscv64-gnu@1.50.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.47.0': - optional: true - '@oxlint/binding-linux-riscv64-musl@1.50.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.47.0': - optional: true - '@oxlint/binding-linux-s390x-gnu@1.50.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.47.0': - optional: true - '@oxlint/binding-linux-x64-gnu@1.50.0': optional: true - '@oxlint/binding-linux-x64-musl@1.47.0': - optional: true - '@oxlint/binding-linux-x64-musl@1.50.0': optional: true - '@oxlint/binding-openharmony-arm64@1.47.0': - optional: true - '@oxlint/binding-openharmony-arm64@1.50.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.47.0': - optional: true - '@oxlint/binding-win32-arm64-msvc@1.50.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.47.0': - optional: true - '@oxlint/binding-win32-ia32-msvc@1.50.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.47.0': - optional: true - '@oxlint/binding-win32-x64-msvc@1.50.0': optional: true @@ -9249,7 +8890,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.9 - '@rushstack/node-core-library@5.14.0(@types/node@22.17.0)': + '@rushstack/node-core-library@5.14.0(@types/node@25.3.0)': dependencies: ajv: 8.13.0 ajv-draft-04: 1.0.0(ajv@8.13.0) @@ -9260,7 +8901,7 @@ snapshots: resolve: 1.22.10 semver: 7.5.4 optionalDependencies: - '@types/node': 22.17.0 + '@types/node': 25.3.0 '@rushstack/rig-package@0.5.3': dependencies: @@ -9274,12 +8915,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.9 - '@rushstack/terminal@0.15.4(@types/node@22.17.0)': + '@rushstack/terminal@0.15.4(@types/node@25.3.0)': dependencies: - '@rushstack/node-core-library': 5.14.0(@types/node@22.17.0) + '@rushstack/node-core-library': 5.14.0(@types/node@25.3.0) supports-color: 8.1.1 optionalDependencies: - '@types/node': 22.17.0 + '@types/node': 25.3.0 '@rushstack/ts-command-line@5.0.2(@types/node@20.19.9)': dependencies: @@ -9290,9 +8931,9 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@rushstack/ts-command-line@5.0.2(@types/node@22.17.0)': + '@rushstack/ts-command-line@5.0.2(@types/node@25.3.0)': dependencies: - '@rushstack/terminal': 0.15.4(@types/node@22.17.0) + '@rushstack/terminal': 0.15.4(@types/node@25.3.0) '@types/argparse': 1.0.38 argparse: 1.0.10 string-argv: 0.3.2 @@ -9338,20 +8979,20 @@ snapshots: dependencies: '@sentry/core': 8.55.0 - '@shikijs/engine-oniguruma@3.9.2': + '@shikijs/engine-oniguruma@3.22.0': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.22.0 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@3.9.2': + '@shikijs/langs@3.22.0': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.22.0 - '@shikijs/themes@3.9.2': + '@shikijs/themes@3.22.0': dependencies: - '@shikijs/types': 3.9.2 + '@shikijs/types': 3.22.0 - '@shikijs/types@3.9.2': + '@shikijs/types@3.22.0': dependencies: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 @@ -9525,7 +9166,7 @@ snapshots: '@types/http-link-header@1.0.7': dependencies: - '@types/node': 22.17.0 + '@types/node': 25.3.0 '@types/json-schema@7.0.15': {} @@ -9551,17 +9192,13 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@22.17.0': - dependencies: - undici-types: 6.21.0 - '@types/node@22.19.11': dependencies: undici-types: 6.21.0 - '@types/node@25.0.3': + '@types/node@25.3.0': dependencies: - undici-types: 7.16.0 + undici-types: 7.18.2 '@types/path-browserify@1.0.3': {} @@ -9602,7 +9239,7 @@ snapshots: '@types/resolve@1.20.2': {} - '@types/semver@7.7.0': {} + '@types/semver@7.7.1': {} '@types/trusted-types@2.0.7': {} @@ -9668,7 +9305,7 @@ snapshots: globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.4 ts-api-utils: 1.4.3(typescript@5.9.2) optionalDependencies: typescript: 5.9.2 @@ -9766,7 +9403,7 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.4 - '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))': + '@vitejs/plugin-react@5.1.4(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -9774,7 +9411,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color @@ -9838,6 +9475,19 @@ snapshots: optionalDependencies: typescript: 5.9.2 + '@vue/language-core@2.2.0(typescript@5.9.3)': + dependencies: + '@volar/language-core': 2.4.22 + '@vue/compiler-dom': 3.5.18 + '@vue/compiler-vue2': 2.7.16 + '@vue/shared': 3.5.18 + alien-signals: 0.4.14 + minimatch: 9.0.5 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + optionalDependencies: + typescript: 5.9.3 + '@vue/shared@3.5.18': {} '@webassemblyjs/ast@1.14.1': @@ -10808,7 +10458,7 @@ snapshots: find-up: 5.0.0 globals: 15.15.0 lodash.memoize: 4.1.2 - semver: 7.7.2 + semver: 7.7.4 eslint-plugin-formatjs@6.2.0(eslint@8.57.1): dependencies: @@ -11383,7 +11033,7 @@ snapshots: is-bun-module@2.0.0: dependencies: - semver: 7.7.2 + semver: 7.7.4 is-callable@1.2.7: {} @@ -11510,9 +11160,9 @@ snapshots: isomorphic.js@0.2.5: {} - isows@1.0.7(ws@8.18.3): + isows@1.0.7(ws@8.19.0): dependencies: - ws: 8.18.3 + ws: 8.19.0 jackspeak@3.4.3: dependencies: @@ -11528,7 +11178,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 25.0.3 + '@types/node': 20.19.9 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -12020,30 +11670,6 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 - oxfmt@0.32.0: - dependencies: - tinypool: 2.1.0 - optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.32.0 - '@oxfmt/binding-android-arm64': 0.32.0 - '@oxfmt/binding-darwin-arm64': 0.32.0 - '@oxfmt/binding-darwin-x64': 0.32.0 - '@oxfmt/binding-freebsd-x64': 0.32.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.32.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.32.0 - '@oxfmt/binding-linux-arm64-gnu': 0.32.0 - '@oxfmt/binding-linux-arm64-musl': 0.32.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.32.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.32.0 - '@oxfmt/binding-linux-riscv64-musl': 0.32.0 - '@oxfmt/binding-linux-s390x-gnu': 0.32.0 - '@oxfmt/binding-linux-x64-gnu': 0.32.0 - '@oxfmt/binding-linux-x64-musl': 0.32.0 - '@oxfmt/binding-openharmony-arm64': 0.32.0 - '@oxfmt/binding-win32-arm64-msvc': 0.32.0 - '@oxfmt/binding-win32-ia32-msvc': 0.32.0 - '@oxfmt/binding-win32-x64-msvc': 0.32.0 - oxfmt@0.35.0: dependencies: tinypool: 2.1.0 @@ -12077,29 +11703,6 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.14.2 '@oxlint-tsgolint/win32-x64': 0.14.2 - oxlint@1.47.0(oxlint-tsgolint@0.14.2): - optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.47.0 - '@oxlint/binding-android-arm64': 1.47.0 - '@oxlint/binding-darwin-arm64': 1.47.0 - '@oxlint/binding-darwin-x64': 1.47.0 - '@oxlint/binding-freebsd-x64': 1.47.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.47.0 - '@oxlint/binding-linux-arm-musleabihf': 1.47.0 - '@oxlint/binding-linux-arm64-gnu': 1.47.0 - '@oxlint/binding-linux-arm64-musl': 1.47.0 - '@oxlint/binding-linux-ppc64-gnu': 1.47.0 - '@oxlint/binding-linux-riscv64-gnu': 1.47.0 - '@oxlint/binding-linux-riscv64-musl': 1.47.0 - '@oxlint/binding-linux-s390x-gnu': 1.47.0 - '@oxlint/binding-linux-x64-gnu': 1.47.0 - '@oxlint/binding-linux-x64-musl': 1.47.0 - '@oxlint/binding-openharmony-arm64': 1.47.0 - '@oxlint/binding-win32-arm64-msvc': 1.47.0 - '@oxlint/binding-win32-ia32-msvc': 1.47.0 - '@oxlint/binding-win32-x64-msvc': 1.47.0 - oxlint-tsgolint: 0.14.2 - oxlint@1.50.0(oxlint-tsgolint@0.14.2): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.50.0 @@ -12457,12 +12060,6 @@ snapshots: quansync@0.2.10: {} - query-string@9.2.2: - dependencies: - decode-uri-component: 0.4.1 - filter-obj: 5.1.0 - split-on-first: 3.0.0 - query-string@9.3.1: dependencies: decode-uri-component: 0.4.1 @@ -12733,31 +12330,31 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4 - rollup-plugin-bundle-stats@4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-bundle-stats@4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@bundle-stats/cli-utils': 4.21.10(core-js@3.48.0) - rollup-plugin-webpack-stats: 2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + rollup-plugin-webpack-stats: 2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) tslib: 2.8.1 optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - core-js - rollup-plugin-stats@1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-stats@1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) - rollup-plugin-webpack-stats@2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + rollup-plugin-webpack-stats@2.1.11(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: - rollup-plugin-stats: 1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)) + rollup-plugin-stats: 1.5.6(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) optionalDependencies: rolldown: 1.0.0-rc.4 rollup: 2.79.2 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) rollup@2.79.2: optionalDependencies: @@ -12938,7 +12535,7 @@ snapshots: dependencies: lru-cache: 6.0.0 - semver@7.7.2: {} + semver@7.7.4: {} serialize-javascript@6.0.2: dependencies: @@ -13481,23 +13078,23 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typedoc-material-theme@1.4.0(typedoc@0.28.9(typescript@5.9.2)): + typedoc-material-theme@1.4.1(typedoc@0.28.17(typescript@5.9.3)): dependencies: '@material/material-color-utilities': 0.3.0 - typedoc: 0.28.9(typescript@5.9.2) + typedoc: 0.28.17(typescript@5.9.3) - typedoc-plugin-valibot@1.0.1(typedoc@0.28.9(typescript@5.9.2)): + typedoc-plugin-valibot@1.0.1(typedoc@0.28.17(typescript@5.9.3)): dependencies: - typedoc: 0.28.9(typescript@5.9.2) + typedoc: 0.28.17(typescript@5.9.3) - typedoc@0.28.9(typescript@5.9.2): + typedoc@0.28.17(typescript@5.9.3): dependencies: - '@gerrit0/mini-shiki': 3.9.1 + '@gerrit0/mini-shiki': 3.22.0 lunr: 2.3.9 markdown-it: 14.1.0 minimatch: 9.0.5 - typescript: 5.9.2 - yaml: 2.8.0 + typescript: 5.9.3 + yaml: 2.8.2 typescript@5.8.2: {} @@ -13518,7 +13115,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.16.0: {} + undici-types@7.18.2: {} undici@7.22.0: {} @@ -13616,7 +13213,7 @@ snapshots: varint@6.0.0: {} - vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-checker@0.12.0(eslint@8.57.1)(meow@13.2.0)(optionator@0.9.4)(oxlint@1.50.0(oxlint-tsgolint@0.14.2))(stylelint@16.23.0(typescript@5.9.3))(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@babel/code-frame': 7.29.0 chokidar: 4.0.3 @@ -13625,7 +13222,7 @@ snapshots: picomatch: 4.0.3 tiny-invariant: 1.3.3 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vscode-uri: 3.1.0 optionalDependencies: eslint: 8.57.1 @@ -13635,7 +13232,7 @@ snapshots: stylelint: 16.23.0(typescript@5.9.3) typescript: 5.9.3 - vite-plugin-compile-time@0.4.6(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-compile-time@0.4.6(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@babel/generator': 7.29.1 '@babel/parser': 7.29.0 @@ -13646,11 +13243,11 @@ snapshots: devalue: 5.1.1 esbuild: 0.24.2 magic-string: 0.30.17 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - supports-color - vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@20.19.9) '@rollup/pluginutils': 5.2.0(rollup@4.57.1) @@ -13663,32 +13260,32 @@ snapshots: magic-string: 0.30.17 typescript: 5.9.2 optionalDependencies: - vite: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-dts@4.5.4(@types/node@22.17.0)(rollup@4.57.1)(typescript@5.9.2)(vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-dts@4.5.4(@types/node@25.3.0)(rollup@4.57.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: - '@microsoft/api-extractor': 7.52.10(@types/node@22.17.0) + '@microsoft/api-extractor': 7.52.10(@types/node@25.3.0) '@rollup/pluginutils': 5.2.0(rollup@4.57.1) '@volar/typescript': 2.4.22 - '@vue/language-core': 2.2.0(typescript@5.9.2) + '@vue/language-core': 2.2.0(typescript@5.9.3) compare-versions: 6.1.1 debug: 4.4.1 kolorist: 1.8.0 local-pkg: 1.1.1 magic-string: 0.30.17 - typescript: 5.9.2 + typescript: 5.9.3 optionalDependencies: - vite: 7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-html@3.2.2(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-html@3.2.2(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@rollup/pluginutils': 4.2.1 colorette: 2.0.20 @@ -13702,27 +13299,27 @@ snapshots: html-minifier-terser: 6.1.0 node-html-parser: 5.4.2 pathe: 0.2.0 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) - vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): + vite-plugin-pwa@1.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2))(workbox-build@7.3.0(@types/babel__core@7.20.5))(workbox-window@7.3.0): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) workbox-build: 7.3.0(@types/babel__core@7.20.5) workbox-window: 7.3.0 transitivePeerDependencies: - supports-color - vite-plugin-require@1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-require@1.2.14(esbuild@0.24.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@babel/generator': 7.28.0 '@babel/parser': 7.28.0 '@babel/traverse': 7.28.0 '@babel/types': 7.28.2 '@vue/compiler-sfc': 3.5.18 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vue-loader: 17.4.2(@vue/compiler-sfc@3.5.18)(webpack@5.101.0(esbuild@0.24.2)) webpack: 5.101.0(esbuild@0.24.2) transitivePeerDependencies: @@ -13733,15 +13330,15 @@ snapshots: - vue - webpack-cli - vite-plugin-static-copy@3.2.0(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0)): + vite-plugin-static-copy@3.2.0(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.15 - vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0) + vite: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) - vite@7.3.1(@types/node@22.17.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): + vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -13750,34 +13347,16 @@ snapshots: rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 22.17.0 + '@types/node': 25.3.0 fsevents: 2.3.3 jiti: 1.21.7 lightningcss: 1.31.1 sass: 1.97.3 sass-embedded: 1.97.3 terser: 5.46.0 - yaml: 2.8.0 + yaml: 2.8.2 - vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 25.0.3 - fsevents: 2.3.3 - jiti: 1.21.7 - lightningcss: 1.31.1 - sass: 1.97.3 - sass-embedded: 1.97.3 - terser: 5.46.0 - yaml: 2.8.0 - - vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.0): + vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2): dependencies: '@oxc-project/runtime': 0.113.0 fdir: 6.5.0(picomatch@4.0.3) @@ -13794,7 +13373,7 @@ snapshots: sass: 1.97.3 sass-embedded: 1.97.3 terser: 5.46.0 - yaml: 2.8.0 + yaml: 2.8.2 vscode-uri@3.1.0: {} @@ -14063,7 +13642,7 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 4.1.0 - ws@8.18.3: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {} @@ -14075,6 +13654,8 @@ snapshots: yaml@2.8.0: {} + yaml@2.8.2: {} + yjs@13.6.27: dependencies: lib0: 0.2.117 From b50a4e72249dc96c41f9aef1574b8b7df309a000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:31:24 +0100 Subject: [PATCH 048/264] pl-api: replace lodash omit/pick with our own util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../lib/client/grouped-notifications.ts | 15 ++------ packages/pl-api/lib/entities/account.ts | 4 +- .../pl-api/lib/entities/admin/announcement.ts | 4 +- packages/pl-api/lib/entities/admin/report.ts | 2 +- .../entities/grouped-notifications-results.ts | 3 +- packages/pl-api/lib/entities/notification.ts | 3 +- packages/pl-api/lib/entities/status.ts | 3 +- .../lib/params/grouped-notifications.ts | 2 +- packages/pl-api/lib/utils/index.ts | 19 ++++++++++ packages/pl-api/package.json | 4 -- pnpm-lock.yaml | 38 ------------------- 11 files changed, 34 insertions(+), 63 deletions(-) create mode 100644 packages/pl-api/lib/utils/index.ts diff --git a/packages/pl-api/lib/client/grouped-notifications.ts b/packages/pl-api/lib/client/grouped-notifications.ts index af1e4cc1c..ddae4af57 100644 --- a/packages/pl-api/lib/client/grouped-notifications.ts +++ b/packages/pl-api/lib/client/grouped-notifications.ts @@ -1,10 +1,9 @@ -import omit from 'lodash.omit'; -import pick from 'lodash.pick'; import * as v from 'valibot'; import { accountSchema, groupedNotificationsResultsSchema } from '../entities'; import { filteredArray } from '../entities/utils'; import { type RequestMeta } from '../request'; +import { pick, omit } from '../utils'; import type { PlApiBaseClient } from '../client-base'; import type { @@ -69,7 +68,7 @@ const _groupNotifications = ( status_id: notification.status?.id, // @ts-expect-error used optional chaining target_id: notification.target?.id, - }); + } as NotificationGroup); } } @@ -224,15 +223,7 @@ const groupedNotifications = ( } return client.notifications.getUnreadNotificationCount( - pick(params || {}, [ - 'max_id', - 'since_id', - 'limit', - 'min_id', - 'types', - 'exclude_types', - 'account_id', - ]), + pick(params || {}, ['limit', 'types', 'exclude_types', 'account_id']), ); }, }; diff --git a/packages/pl-api/lib/entities/account.ts b/packages/pl-api/lib/entities/account.ts index 912718183..5649ec9ad 100644 --- a/packages/pl-api/lib/entities/account.ts +++ b/packages/pl-api/lib/entities/account.ts @@ -1,6 +1,6 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../utils'; import { isDefaultAvatar, isDefaultHeader } from '../utils/accounts'; import { guessFqn } from '../utils/domain'; @@ -107,7 +107,7 @@ const preprocessAccount = v.transform((account: any) => { ...pick(account.akkoma || {}, ['permit_followback']), is_cat: isCat, speak_as_cat: speakAsCat, - ...(pick(account.other_settings || {}), ['birthday', 'location']), + ...pick(account.other_settings || {}, ['birthday', 'location']), __meta: pick(account, ['pleroma', 'source']), ...account, display_name: diff --git a/packages/pl-api/lib/entities/admin/announcement.ts b/packages/pl-api/lib/entities/admin/announcement.ts index ef6b20fba..41bac606f 100644 --- a/packages/pl-api/lib/entities/admin/announcement.ts +++ b/packages/pl-api/lib/entities/admin/announcement.ts @@ -1,6 +1,6 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../../utils'; import { announcementSchema } from '../announcement'; /** @@ -11,7 +11,7 @@ const adminAnnouncementSchema = v.pipe( v.any(), v.transform((announcement: any) => ({ ...announcement, - ...pick(announcement.pleroma, 'raw_content'), + ...pick(announcement.pleroma, ['raw_content']), })), v.object({ ...announcementSchema.entries, diff --git a/packages/pl-api/lib/entities/admin/report.ts b/packages/pl-api/lib/entities/admin/report.ts index fc769b6e5..39e72b3d2 100644 --- a/packages/pl-api/lib/entities/admin/report.ts +++ b/packages/pl-api/lib/entities/admin/report.ts @@ -1,6 +1,6 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../../utils'; import { ruleSchema } from '../rule'; import { statusWithoutAccountSchema } from '../status'; import { datetimeSchema, filteredArray } from '../utils'; diff --git a/packages/pl-api/lib/entities/grouped-notifications-results.ts b/packages/pl-api/lib/entities/grouped-notifications-results.ts index 5d51c6c19..3ab80adb1 100644 --- a/packages/pl-api/lib/entities/grouped-notifications-results.ts +++ b/packages/pl-api/lib/entities/grouped-notifications-results.ts @@ -1,6 +1,7 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../utils'; + import { accountSchema } from './account'; import { accountWarningSchema } from './account-warning'; import { chatMessageSchema } from './chat-message'; diff --git a/packages/pl-api/lib/entities/notification.ts b/packages/pl-api/lib/entities/notification.ts index e260045fe..27248c3de 100644 --- a/packages/pl-api/lib/entities/notification.ts +++ b/packages/pl-api/lib/entities/notification.ts @@ -1,6 +1,7 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../utils'; + import { accountSchema } from './account'; import { accountWarningSchema } from './account-warning'; import { chatMessageSchema } from './chat-message'; diff --git a/packages/pl-api/lib/entities/status.ts b/packages/pl-api/lib/entities/status.ts index 3547fb52a..32641f06a 100644 --- a/packages/pl-api/lib/entities/status.ts +++ b/packages/pl-api/lib/entities/status.ts @@ -1,6 +1,7 @@ -import pick from 'lodash.pick'; import * as v from 'valibot'; +import { pick } from '../utils'; + import { type Account, accountSchema } from './account'; import { customEmojiSchema } from './custom-emoji'; import { emojiReactionSchema } from './emoji-reaction'; diff --git a/packages/pl-api/lib/params/grouped-notifications.ts b/packages/pl-api/lib/params/grouped-notifications.ts index a46ab2024..447fae647 100644 --- a/packages/pl-api/lib/params/grouped-notifications.ts +++ b/packages/pl-api/lib/params/grouped-notifications.ts @@ -9,7 +9,7 @@ interface GetGroupedNotificationsParams extends PaginationParams { /** Types to exclude from the results. */ exclude_types?: Array; /** Return only notifications received from the specified account. */ - acccount_id?: string; + account_id?: string; /** One of `full` (default) or `partial_avatars`. When set to `partial_avatars`, some accounts will not be rendered in full in the returned `accounts` list but will be instead returned in stripped-down form in the `partial_accounts` list. The most recent account in a notification group is always rendered in full in the `accounts` attribute. */ expand_accounts?: 'full' | 'partial_avatars'; /** Restrict which notification types can be grouped. Use this if there are notification types for which your client does not support grouping. If omitted, the server will group notifications of all types it supports (currently, `favourite`, `follow` and `reblog`). If you do not want any notification grouping, use GET `/api/v1/notifications` instead. Notifications that would be grouped if not for this parameter will instead be returned as individual single-notification groups with a unique `group_key` that can be assumed to be of the form `ungrouped-{notification_id}`. Please note that neither the streaming API nor the individual notification APIs are aware of this parameter and will always include a “proper” `group_key` that can be different from what is returned here, meaning that you may have to ignore `group_key` for such notifications that you do not want grouped and use `ungrouped-{notification_id}` instead for consistency. */ diff --git a/packages/pl-api/lib/utils/index.ts b/packages/pl-api/lib/utils/index.ts new file mode 100644 index 000000000..05936ec4d --- /dev/null +++ b/packages/pl-api/lib/utils/index.ts @@ -0,0 +1,19 @@ +const pick = , K extends keyof T>(obj: T, keys: K[]): Pick => { + const result = {} as Pick; + for (const key of keys) { + if (key in obj) { + result[key] = obj[key]; + } + } + return result; +}; + +const omit = , K extends string>(obj: T, keys: K[]): Omit => { + const result = { ...obj }; + for (const key of keys) { + delete result[key]; + } + return result; +}; + +export { pick, omit }; diff --git a/packages/pl-api/package.json b/packages/pl-api/package.json index 4ada82496..2713706f0 100644 --- a/packages/pl-api/package.json +++ b/packages/pl-api/package.json @@ -30,8 +30,6 @@ "blurhash": "^2.0.5", "http-link-header": "^1.1.3", "isows": "^1.0.7", - "lodash.omit": "^4.5.0", - "lodash.pick": "^4.4.0", "object-to-formdata": "^4.5.1", "query-string": "^9.3.1", "semver": "^7.7.4", @@ -39,8 +37,6 @@ }, "devDependencies": { "@types/http-link-header": "^1.0.7", - "@types/lodash.omit": "^4.5.9", - "@types/lodash.pick": "^4.4.9", "@types/node": "^25.3.0", "@types/semver": "^7.7.1", "oxfmt": "^0.35.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 338b2dfeb..acc9be920 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,12 +33,6 @@ importers: isows: specifier: ^1.0.7 version: 1.0.7(ws@8.19.0) - lodash.omit: - specifier: ^4.5.0 - version: 4.5.0 - lodash.pick: - specifier: ^4.4.0 - version: 4.4.0 object-to-formdata: specifier: ^4.5.1 version: 4.5.1 @@ -55,12 +49,6 @@ importers: '@types/http-link-header': specifier: ^1.0.7 version: 1.0.7 - '@types/lodash.omit': - specifier: ^4.5.9 - version: 4.5.9 - '@types/lodash.pick': - specifier: ^4.4.9 - version: 4.4.9 '@types/node': specifier: ^25.3.0 version: 25.3.0 @@ -2800,12 +2788,6 @@ packages: '@types/leaflet@1.9.21': resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} - '@types/lodash.omit@4.5.9': - resolution: {integrity: sha512-zuAVFLUPJMOzsw6yawshsYGgq2hWUHtsZgeXHZmSFhaQQFC6EQ021uDKHkSjOpNhSvtNSU9165/o3o/Q51GpTw==} - - '@types/lodash.pick@4.4.9': - resolution: {integrity: sha512-hDpr96x9xHClwy1KX4/RXRejqjDFTEGbEMT3t6wYSYeFDzxmMnSKB/xHIbktRlPj8Nii2g8L5dtFDRaNFBEzUQ==} - '@types/lodash@4.17.20': resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} @@ -4828,14 +4810,6 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.omit@4.5.0: - resolution: {integrity: sha512-XeqSp49hNGmlkj2EJlfrQFIzQ6lXdNro9sddtQzcJY8QaoC2GO0DT7xaIokHeyM+mIT0mPMlPvkYzg2xCuHdZg==} - deprecated: This package is deprecated. Use destructuring assignment syntax instead. - - lodash.pick@4.4.0: - resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==} - deprecated: This package is deprecated. Use destructuring assignment syntax instead. - lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} @@ -9176,14 +9150,6 @@ snapshots: dependencies: '@types/geojson': 7946.0.16 - '@types/lodash.omit@4.5.9': - dependencies: - '@types/lodash': 4.17.20 - - '@types/lodash.pick@4.4.9': - dependencies: - '@types/lodash': 4.17.20 - '@types/lodash@4.17.20': {} '@types/lodash@4.17.24': {} @@ -11419,10 +11385,6 @@ snapshots: lodash.merge@4.6.2: {} - lodash.omit@4.5.0: {} - - lodash.pick@4.4.0: {} - lodash.sortby@4.7.0: {} lodash.truncate@4.4.2: {} From 49eba07ca58551ee3932ff65acaebf7f836b6757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:32:24 +0100 Subject: [PATCH 049/264] update oxfmt config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/.oxfmtrc.json | 4 ++-- packages/pl-fe/.oxfmtrc.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/pl-api/.oxfmtrc.json b/packages/pl-api/.oxfmtrc.json index f3af56790..abd2dba0b 100644 --- a/packages/pl-api/.oxfmtrc.json +++ b/packages/pl-api/.oxfmtrc.json @@ -4,8 +4,8 @@ "printWidth": null, "singleQuote": true, "arrowParens": null, - "experimentalSortImports": { - "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"] + "sortImports": { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type"] }, "tabWidth": 2 } diff --git a/packages/pl-fe/.oxfmtrc.json b/packages/pl-fe/.oxfmtrc.json index 3bd481651..5b70eeb95 100644 --- a/packages/pl-fe/.oxfmtrc.json +++ b/packages/pl-fe/.oxfmtrc.json @@ -4,8 +4,8 @@ "printWidth": null, "singleQuote": true, "arrowParens": null, - "experimentalSortImports": { - "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"] + "sortImports": { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type"] }, "tabWidth": 2, "jsxSingleQuote": true, From 3d178e87cb141efd7270aaa5162a29c1c3b1cf48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:35:10 +0100 Subject: [PATCH 050/264] nicolium: update oxfmt config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/.oxfmtrc.json | 2 +- packages/pl-fe/.oxfmtrc.json | 2 +- packages/pl-fe/src/components/ui/spinner.tsx | 1 + packages/pl-fe/src/features/birthdays/date-picker.ts | 1 + packages/pl-fe/src/features/ui/index.tsx | 1 - 5 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/pl-api/.oxfmtrc.json b/packages/pl-api/.oxfmtrc.json index abd2dba0b..bbca529a2 100644 --- a/packages/pl-api/.oxfmtrc.json +++ b/packages/pl-api/.oxfmtrc.json @@ -5,7 +5,7 @@ "singleQuote": true, "arrowParens": null, "sortImports": { - "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type"] + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type", "style"] }, "tabWidth": 2 } diff --git a/packages/pl-fe/.oxfmtrc.json b/packages/pl-fe/.oxfmtrc.json index 5b70eeb95..96daafabf 100644 --- a/packages/pl-fe/.oxfmtrc.json +++ b/packages/pl-fe/.oxfmtrc.json @@ -5,7 +5,7 @@ "singleQuote": true, "arrowParens": null, "sortImports": { - "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type"] + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type", "style"] }, "tabWidth": 2, "jsxSingleQuote": true, diff --git a/packages/pl-fe/src/components/ui/spinner.tsx b/packages/pl-fe/src/components/ui/spinner.tsx index 3e6f0e2da..87bf6bf37 100644 --- a/packages/pl-fe/src/components/ui/spinner.tsx +++ b/packages/pl-fe/src/components/ui/spinner.tsx @@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl'; import Stack from './stack'; import Text from './text'; + import './spinner.css'; interface ISpinner { diff --git a/packages/pl-fe/src/features/birthdays/date-picker.ts b/packages/pl-fe/src/features/birthdays/date-picker.ts index 9a4845be4..c2ca0f7a7 100644 --- a/packages/pl-fe/src/features/birthdays/date-picker.ts +++ b/packages/pl-fe/src/features/birthdays/date-picker.ts @@ -1,4 +1,5 @@ import DatePicker from 'react-datepicker'; + import 'react-datepicker/dist/react-datepicker.css'; export { DatePicker as default }; diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index f32802024..4f8d28fed 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -40,7 +40,6 @@ import { DropdownNavigation, StatusHoverCard, } from './util/async-components'; - // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '@/components/status'; From 5ef7b1e4376d884f43cd9dc31a29cb526095e746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 13:40:25 +0100 Subject: [PATCH 051/264] nicolium: oxlint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/.oxlintrc.json | 3 ++- packages/pl-fe/.oxlintrc.json | 1 + packages/pl-fe/src/components/parsed-content.tsx | 4 ++-- packages/pl-fe/src/components/preview-card.tsx | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/pl-api/.oxlintrc.json b/packages/pl-api/.oxlintrc.json index 478574a87..92d30f12c 100644 --- a/packages/pl-api/.oxlintrc.json +++ b/packages/pl-api/.oxlintrc.json @@ -22,7 +22,8 @@ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" } - ] + ], + "no-shadow": "off" }, "settings": { "jsx-a11y": { diff --git a/packages/pl-fe/.oxlintrc.json b/packages/pl-fe/.oxlintrc.json index 653ddbe26..137676f89 100644 --- a/packages/pl-fe/.oxlintrc.json +++ b/packages/pl-fe/.oxlintrc.json @@ -43,6 +43,7 @@ "no-unsafe-type-assertion": "warn", "require-array-sort-compare": "off", "unbound-method": "warn", + "no-shadow": "off", "formatjs/enforce-default-message": "error", "formatjs/enforce-id": "error", diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index d66150dbd..cb7cbaf7d 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -6,7 +6,7 @@ import parse, { domToReact, type DOMNode, } from 'html-react-parser'; -import DOMPurify from 'isomorphic-dompurify'; +import { sanitize } from 'isomorphic-dompurify'; import groupBy from 'lodash/groupBy'; import minBy from 'lodash/minBy'; import React from 'react'; @@ -353,7 +353,7 @@ function parseContent( }; let content = parse( - DOMPurify.sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), + sanitize(html, { ADD_ATTR: ['target'], USE_PROFILES: { html: true } }), options, ); diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index df8fc9282..9203a82e7 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -1,6 +1,6 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; -import DOMPurify from 'isomorphic-dompurify'; +import { sanitize } from 'isomorphic-dompurify'; import { type MediaAttachment, type PreviewCard as CardEntity, @@ -65,7 +65,7 @@ interface IPreviewCardVideo { const PreviewCardVideo: React.FC = React.memo( React.forwardRef(({ card }, ref) => { - const html = DOMPurify.sanitize(handleIframeUrl(card.html, card.url, card.provider_name), { + const html = sanitize(handleIframeUrl(card.html, card.url, card.provider_name), { ADD_TAGS: ['iframe'], ADD_ATTR: ['allow', 'allowfullscreen', 'referrerpolicy'], }); From bbf03251461f9a8f7b139089e12fb873f409568b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 14:01:33 +0100 Subject: [PATCH 052/264] pl-hooks: migrate from eslint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- package.json | 10 +- packages/pl-hooks/.eslintignore | 7 - packages/pl-hooks/.eslintrc.json | 214 --- packages/pl-hooks/.oxfmtrc.json | 12 + packages/pl-hooks/.oxlintrc.json | 80 + packages/pl-hooks/README.md | 2 +- .../lib/hooks/account-lists/use-directory.ts | 41 +- .../lib/hooks/accounts/use-account-lookup.ts | 33 +- .../accounts/use-account-relationship.ts | 17 +- .../lib/hooks/accounts/use-account.ts | 39 +- .../lib/hooks/instance/use-instance.ts | 15 +- .../instance/use-translation-languages.ts | 33 +- .../pl-hooks/lib/hooks/markers/use-markers.ts | 17 +- .../markers/use-update-marker-mutation.ts | 37 +- .../notifications/use-notification-list.ts | 83 +- .../hooks/notifications/use-notification.ts | 59 +- packages/pl-hooks/lib/hooks/polls/use-poll.ts | 17 +- .../pl-hooks/lib/hooks/search/use-search.ts | 120 +- .../lib/hooks/statuses/use-status-history.ts | 29 +- .../lib/hooks/statuses/use-status-quotes.ts | 29 +- .../hooks/statuses/use-status-translation.ts | 22 +- .../pl-hooks/lib/hooks/statuses/use-status.ts | 65 +- packages/pl-hooks/lib/importer.ts | 95 +- .../pl-hooks/lib/normalizers/notification.ts | 132 +- .../pl-hooks/lib/normalizers/status-list.ts | 11 +- packages/pl-hooks/lib/normalizers/status.ts | 47 +- packages/pl-hooks/lib/utils/queries.ts | 9 +- packages/pl-hooks/package.json | 60 +- packages/pl-hooks/tsconfig-build.json | 6 +- packages/pl-hooks/tsconfig.json | 4 +- packages/pl-hooks/vite.config.ts | 4 +- pnpm-lock.yaml | 1319 ++--------------- 32 files changed, 882 insertions(+), 1786 deletions(-) delete mode 100644 packages/pl-hooks/.eslintignore delete mode 100644 packages/pl-hooks/.eslintrc.json create mode 100644 packages/pl-hooks/.oxfmtrc.json create mode 100644 packages/pl-hooks/.oxlintrc.json diff --git a/package.json b/package.json index ba7e55f37..aa544aa30 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,11 @@ }, "devDependencies": { "husky": "^9.0.0", - "lint-staged": ">=10" + "lint-staged": "^16.2.7" }, "resolutions": { - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", - "glob-parent": "^6.0.1", - "jsonwebtoken": "^9.0.0", - "loader-utils": "^2.0.3" + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "glob-parent": "^6.0.2" } } diff --git a/packages/pl-hooks/.eslintignore b/packages/pl-hooks/.eslintignore deleted file mode 100644 index 256b5ff45..000000000 --- a/packages/pl-hooks/.eslintignore +++ /dev/null @@ -1,7 +0,0 @@ -/node_modules/** -/dist/** -/static/** -/public/** -/tmp/** -/coverage/** -/custom/** diff --git a/packages/pl-hooks/.eslintrc.json b/packages/pl-hooks/.eslintrc.json deleted file mode 100644 index 8f5bed6ac..000000000 --- a/packages/pl-hooks/.eslintrc.json +++ /dev/null @@ -1,214 +0,0 @@ -{ - "root": true, - "extends": [ - "eslint:recommended", - "plugin:import/typescript", - "plugin:compat/recommended" - ], - "env": { - "browser": true, - "node": true, - "es6": true, - "jest": true - }, - "globals": { - "ATTACHMENT_HOST": false - }, - "plugins": [ - "import", - "promise", - "@typescript-eslint" - ], - "parserOptions": { - "sourceType": "module", - "ecmaFeatures": { - "experimentalObjectRestSpread": true - }, - "ecmaVersion": 2018 - }, - "settings": { - "import/extensions": [ - ".js", - ".cjs", - ".mjs", - ".ts" - ], - "import/ignore": [ - "node_modules", - "\\.(css|scss|json)$" - ], - "import/resolver": { - "typescript": true, - "node": true - }, - "polyfills": [ - "es:all", - "fetch", - "IntersectionObserver", - "Promise", - "ResizeObserver", - "URL", - "URLSearchParams" - ], - "tailwindcss": { - "config": "tailwind.config.ts" - } - }, - "rules": { - "brace-style": "error", - "comma-dangle": [ - "error", - "always-multiline" - ], - "comma-spacing": [ - "warn", - { - "before": false, - "after": true - } - ], - "comma-style": [ - "warn", - "last" - ], - "import/no-duplicates": "error", - "space-before-function-paren": [ - "error", - "never" - ], - "space-infix-ops": "error", - "space-in-parens": [ - "error", - "never" - ], - "keyword-spacing": "error", - "dot-notation": "error", - "eqeqeq": "error", - "indent": [ - "error", - 2, - { - "SwitchCase": 1, - "ignoredNodes": [ - "TemplateLiteral" - ] - } - ], - "key-spacing": [ - "error", - { - "mode": "minimum" - } - ], - "no-catch-shadow": "error", - "no-cond-assign": "error", - "no-console": [ - "warn", - { - "allow": [ - "error", - "warn" - ] - } - ], - "no-extra-semi": "error", - "no-const-assign": "error", - "no-fallthrough": "error", - "no-irregular-whitespace": "error", - "no-loop-func": "error", - "no-mixed-spaces-and-tabs": "error", - "no-nested-ternary": "warn", - "no-trailing-spaces": "error", - "no-undef": "error", - "no-unreachable": "error", - "no-unused-expressions": "error", - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error", - { - "vars": "all", - "args": "none", - "ignoreRestSiblings": true - } - ], - "no-useless-escape": "warn", - "no-var": "error", - "object-curly-spacing": [ - "error", - "always" - ], - "padded-blocks": [ - "error", - { - "classes": "always" - } - ], - "prefer-const": "error", - "quotes": [ - "error", - "single" - ], - "semi": "error", - "space-unary-ops": [ - "error", - { - "words": true, - "nonwords": false - } - ], - "strict": "off", - "valid-typeof": "error", - "import/extensions": [ - "error", - "always", - { - "js": "never", - "mjs": "ignorePackages", - "ts": "never" - } - ], - "import/newline-after-import": "error", - "import/no-extraneous-dependencies": "error", - "import/no-unresolved": "error", - "import/no-webpack-loader-syntax": "error", - "import/order": [ - "error", - { - "groups": [ - "builtin", - "external", - "internal", - "parent", - "sibling", - "index", - "object", - "type" - ], - "newlines-between": "always", - "alphabetize": { - "order": "asc" - } - } - ], - "@typescript-eslint/member-delimiter-style": "error", - "promise/catch-or-return": "error", - "sort-imports": [ - "error", - { - "ignoreCase": true, - "ignoreDeclarationSort": true - } - ], - "eol-last": "error" - }, - "overrides": [ - { - "files": ["**/*.ts"], - "rules": { - "no-undef": "off", - "space-before-function-paren": "off" - }, - "parser": "@typescript-eslint/parser" - } - ] -} \ No newline at end of file diff --git a/packages/pl-hooks/.oxfmtrc.json b/packages/pl-hooks/.oxfmtrc.json new file mode 100644 index 000000000..a14876bb2 --- /dev/null +++ b/packages/pl-hooks/.oxfmtrc.json @@ -0,0 +1,12 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "ignorePatterns": [], + "printWidth": null, + "singleQuote": true, + "arrowParens": null, + "sortImports": { + "groups": ["builtin", "external", "internal", "parent", "sibling", "index", "type", "style"] + }, + "tabWidth": 2, + "jsxSingleQuote": true +} diff --git a/packages/pl-hooks/.oxlintrc.json b/packages/pl-hooks/.oxlintrc.json new file mode 100644 index 000000000..eaacf33ae --- /dev/null +++ b/packages/pl-hooks/.oxlintrc.json @@ -0,0 +1,80 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "import", "promise"], + "categories": { + "correctness": "error", + "suspicious": "error", + "pedantic": "warn", + "perf": "warn" + }, + "rules": { + "always-return": "off", + "ban-types": "off", + "ban-ts-comment": "off", + "max-dependencies": "off", + "max-lines": "off", + "max-lines-per-function": "off", + "no-await-in-loop": "off", + "no-else-return": "off", + "no-inline-comments": "off", + "no-named-as-default": "off", + "no-negated-condition": "off", + "no-new": "warn", + "no-non-null-asserted-optional-chain": "warn", + "no-promise-executor-return": "off", + "no-unassigned-import": "off", + "no-unused-vars": [ + "error", + { + "vars": "all", + "args": "none", + "ignoreRestSiblings": true, + "caughtErrors": "none", + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "prefer-ts-expect-error": "off", + "sort-vars": "off", + + "no-floating-promises": "warn", + "no-redundant-type-constituents": "warn", + "no-unsafe-type-assertion": "warn", + "require-array-sort-compare": "off", + "unbound-method": "warn", + "no-shadow": "off" + }, + "settings": { + "jsx-a11y": { + "polymorphicPropName": null, + "components": {}, + "attributes": {} + }, + "next": { + "rootDir": [] + }, + "react": { + "formComponents": [], + "linkComponents": [], + "version": null, + "componentWrapperFunctions": [] + }, + "jsdoc": { + "ignorePrivate": false, + "ignoreInternal": false, + "ignoreReplacesDocs": true, + "overrideReplacesDocs": true, + "augmentsExtendsReplacesDocs": false, + "implementsReplacesDocs": false, + "exemptDestructuredRootsFromChecks": false, + "tagNamePreference": {} + } + }, + "env": { + "builtin": true, + "browser": true, + "es6": true + }, + "globals": {}, + "ignorePatterns": [] +} diff --git a/packages/pl-hooks/README.md b/packages/pl-hooks/README.md index 66f49a530..8929aaba4 100644 --- a/packages/pl-hooks/README.md +++ b/packages/pl-hooks/README.md @@ -15,4 +15,4 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . \ No newline at end of file +along with this program. If not, see . diff --git a/packages/pl-hooks/lib/hooks/account-lists/use-directory.ts b/packages/pl-hooks/lib/hooks/account-lists/use-directory.ts index e9e1dc926..95c66d775 100644 --- a/packages/pl-hooks/lib/hooks/account-lists/use-directory.ts +++ b/packages/pl-hooks/lib/hooks/account-lists/use-directory.ts @@ -1,27 +1,34 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; const useDirectory = (order: 'active' | 'new', local: boolean = false) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - return useInfiniteQuery({ - queryKey: ['accountsLists', 'directory', order, local], - queryFn: ({ pageParam: offset }) => client.instance.profileDirectory({ - order, - local, - offset, - }).then((accounts) => { - importEntities({ accounts }); - return accounts.map(({ id }) => id); - }), - initialPageParam: 0, - getNextPageParam: (_, allPages) => allPages.at(-1)?.length === 0 ? undefined : allPages.flat().length, - select: (data) => data?.pages.flat(), - }, queryClient); + return useInfiniteQuery( + { + queryKey: ['accountsLists', 'directory', order, local], + queryFn: ({ pageParam: offset }) => + client.instance + .profileDirectory({ + order, + local, + offset, + }) + .then((accounts) => { + importEntities({ accounts }); + return accounts.map(({ id }) => id); + }), + initialPageParam: 0, + getNextPageParam: (_, allPages) => + allPages.at(-1)?.length === 0 ? undefined : allPages.flat().length, + select: (data) => data?.pages.flat(), + }, + queryClient, + ); }; export { useDirectory }; diff --git a/packages/pl-hooks/lib/hooks/accounts/use-account-lookup.ts b/packages/pl-hooks/lib/hooks/accounts/use-account-lookup.ts index 1184ea107..4bd472d51 100644 --- a/packages/pl-hooks/lib/hooks/accounts/use-account-lookup.ts +++ b/packages/pl-hooks/lib/hooks/accounts/use-account-lookup.ts @@ -1,8 +1,8 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; import { useAccount, type UseAccountOpts } from './use-account'; @@ -11,19 +11,22 @@ const useAccountLookup = (acct?: string, opts: UseAccountOpts = {}) => { const queryClient = usePlHooksQueryClient(); const { features } = client; - const accountIdQuery = useQuery({ - queryKey: ['accounts', 'byAcct', acct?.toLocaleLowerCase()], - queryFn: () => ( - features.accountByUsername && !features.accountLookup - ? client.accounts.getAccount(acct!) - : client.accounts.lookupAccount(acct!) - ).then((account) => { - importEntities({ accounts: [account] }); + const accountIdQuery = useQuery( + { + queryKey: ['accounts', 'byAcct', acct?.toLocaleLowerCase()], + queryFn: () => + (features.accountByUsername && !features.accountLookup + ? client.accounts.getAccount(acct!) + : client.accounts.lookupAccount(acct!) + ).then((account) => { + importEntities({ accounts: [account] }); - return account.id; - }), - enabled: !!acct, - }, queryClient); + return account.id; + }), + enabled: !!acct, + }, + queryClient, + ); return useAccount(accountIdQuery.data, opts); }; diff --git a/packages/pl-hooks/lib/hooks/accounts/use-account-relationship.ts b/packages/pl-hooks/lib/hooks/accounts/use-account-relationship.ts index f4c3c82b4..d80f323ce 100644 --- a/packages/pl-hooks/lib/hooks/accounts/use-account-relationship.ts +++ b/packages/pl-hooks/lib/hooks/accounts/use-account-relationship.ts @@ -1,17 +1,20 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; const useAccountRelationship = (accountId?: string) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - return useQuery({ - queryKey: ['relationships', 'entities', accountId], - queryFn: async () => (await client.accounts.getRelationships([accountId!]))[0], - enabled: !!accountId, - }, queryClient); + return useQuery( + { + queryKey: ['relationships', 'entities', accountId], + queryFn: async () => (await client.accounts.getRelationships([accountId!]))[0], + enabled: !!accountId, + }, + queryClient, + ); }; export { useAccountRelationship }; diff --git a/packages/pl-hooks/lib/hooks/accounts/use-account.ts b/packages/pl-hooks/lib/hooks/accounts/use-account.ts index f621c9930..5142243c2 100644 --- a/packages/pl-hooks/lib/hooks/accounts/use-account.ts +++ b/packages/pl-hooks/lib/hooks/accounts/use-account.ts @@ -1,9 +1,9 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { queryClient, usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; -import { normalizeAccount, type NormalizedAccount } from 'pl-hooks/normalizers/account'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { queryClient, usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; +import { normalizeAccount, type NormalizedAccount } from '@/normalizers/account'; import { useAccountRelationship } from './use-account-relationship'; @@ -19,18 +19,22 @@ interface UseAccountOpts { withMoveTarget?: boolean; } -type UseAccountQueryResult = Omit, 'data'> & { data: Account | undefined }; +type UseAccountQueryResult = Omit, 'data'> & { + data: Account | undefined; +}; const useAccount = (accountId?: string, opts: UseAccountOpts = {}): UseAccountQueryResult => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - const accountQuery = useQuery({ - queryKey: ['accounts', 'entities', accountId], - queryFn: () => client.accounts.getAccount(accountId!) - .then(normalizeAccount), - enabled: !!accountId, - }, queryClient); + const accountQuery = useQuery( + { + queryKey: ['accounts', 'entities', accountId], + queryFn: () => client.accounts.getAccount(accountId!).then(normalizeAccount), + enabled: !!accountId, + }, + queryClient, + ); const relationshipQuery = useAccountRelationship(opts.withRelationship ? accountId : undefined); @@ -40,7 +44,14 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}): UseAccountQu data = { ...accountQuery.data, relationship: relationshipQuery.data || null, - moved: opts.withMoveTarget && queryClient.getQueryData(['accounts', 'entities', accountQuery.data?.moved_id]) as Account || null, + moved: + (opts.withMoveTarget && + (queryClient.getQueryData([ + 'accounts', + 'entities', + accountQuery.data?.moved_id, + ]) as Account)) || + null, }; } @@ -50,8 +61,8 @@ const useAccount = (accountId?: string, opts: UseAccountOpts = {}): UseAccountQu const prefetchAccount = (client: PlApiClient, accountId: string) => queryClient.prefetchQuery({ queryKey: ['accounts', 'entities', accountId], - queryFn: () => client.accounts.getAccount(accountId!) - .then(account => { + queryFn: () => + client.accounts.getAccount(accountId!).then((account) => { importEntities({ accounts: [account] }, { withParents: false }); return normalizeAccount(account); diff --git a/packages/pl-hooks/lib/hooks/instance/use-instance.ts b/packages/pl-hooks/lib/hooks/instance/use-instance.ts index fcadcf90f..4ca75d2e8 100644 --- a/packages/pl-hooks/lib/hooks/instance/use-instance.ts +++ b/packages/pl-hooks/lib/hooks/instance/use-instance.ts @@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query'; import { instanceSchema } from 'pl-api'; import * as v from 'valibot'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; const initialData = v.parse(instanceSchema, {}); @@ -11,10 +11,13 @@ const useInstance = () => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - const query = useQuery({ - queryKey: ['instance'], - queryFn: client.instance.getInstance, - }, queryClient); + const query = useQuery( + { + queryKey: ['instance'], + queryFn: client.instance.getInstance, + }, + queryClient, + ); return { ...query, data: query.data || initialData }; }; diff --git a/packages/pl-hooks/lib/hooks/instance/use-translation-languages.ts b/packages/pl-hooks/lib/hooks/instance/use-translation-languages.ts index 28fb0bf9c..c12c973c1 100644 --- a/packages/pl-hooks/lib/hooks/instance/use-translation-languages.ts +++ b/packages/pl-hooks/lib/hooks/instance/use-translation-languages.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; import { useInstance } from './use-instance'; @@ -10,29 +10,32 @@ const useTranslationLanguages = () => { const queryClient = usePlHooksQueryClient(); const { data: instance } = useInstance(); - const { - allow_unauthenticated: allowUnauthenticated, - } = instance!.pleroma.metadata.translation; + const { allow_unauthenticated: allowUnauthenticated } = instance!.pleroma.metadata.translation; const getTranslationLanguages = async () => { const metadata = instance!.pleroma.metadata; if (metadata.translation.source_languages?.length) { - return Object.fromEntries(metadata.translation.source_languages.map(source => [ - source, - metadata.translation.target_languages!.filter(lang => lang !== source), - ])); + return Object.fromEntries( + metadata.translation.source_languages.map((source) => [ + source, + metadata.translation.target_languages!.filter((lang) => lang !== source), + ]), + ); } return client.instance.getInstanceTranslationLanguages(); }; - return useQuery({ - queryKey: ['instance', 'translationLanguages'], - queryFn: getTranslationLanguages, - placeholderData: {}, - enabled: allowUnauthenticated && client.features.translations, - }, queryClient); + return useQuery( + { + queryKey: ['instance', 'translationLanguages'], + queryFn: getTranslationLanguages, + placeholderData: {}, + enabled: allowUnauthenticated && client.features.translations, + }, + queryClient, + ); }; export { useTranslationLanguages }; diff --git a/packages/pl-hooks/lib/hooks/markers/use-markers.ts b/packages/pl-hooks/lib/hooks/markers/use-markers.ts index a6e8c90af..46c85c210 100644 --- a/packages/pl-hooks/lib/hooks/markers/use-markers.ts +++ b/packages/pl-hooks/lib/hooks/markers/use-markers.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { queryClient, usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { queryClient, usePlHooksQueryClient } from '@/contexts/query-client'; import type { PlApiClient } from 'pl-api'; @@ -11,16 +11,19 @@ const useMarker = (timeline: Timeline) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - return useQuery({ - queryKey: ['markers', timeline], - queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]), - }, queryClient); + return useQuery( + { + queryKey: ['markers', timeline], + queryFn: () => client.timelines.getMarkers([timeline]).then((markers) => markers[timeline]), + }, + queryClient, + ); }; const prefetchMarker = (client: PlApiClient, timeline: 'home' | 'notifications') => queryClient.prefetchQuery({ queryKey: ['markers', timeline], - queryFn: () => client.timelines.getMarkers([timeline]).then(markers => markers[timeline]), + queryFn: () => client.timelines.getMarkers([timeline]).then((markers) => markers[timeline]), }); export { useMarker, prefetchMarker, type Timeline }; diff --git a/packages/pl-hooks/lib/hooks/markers/use-update-marker-mutation.ts b/packages/pl-hooks/lib/hooks/markers/use-update-marker-mutation.ts index e83a76cad..2d3f44c39 100644 --- a/packages/pl-hooks/lib/hooks/markers/use-update-marker-mutation.ts +++ b/packages/pl-hooks/lib/hooks/markers/use-update-marker-mutation.ts @@ -1,7 +1,7 @@ import { useMutation } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; import type { Timeline } from './use-markers'; import type { Marker } from 'pl-api'; @@ -10,18 +10,27 @@ const useUpdateMarkerMutation = (timeline: Timeline) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - return useMutation({ - mutationFn: (lastReadId: string) => client.timelines.saveMarkers({ - [timeline]: { - last_read_id: lastReadId, - }, - }), - retry: false, - onMutate: (lastReadId) => queryClient.setQueryData(['markers', timeline], (marker) => marker ? ({ - ...marker, - last_read_id: lastReadId, - }) : undefined), - }, queryClient); + return useMutation( + { + mutationFn: (lastReadId: string) => + client.timelines.saveMarkers({ + [timeline]: { + last_read_id: lastReadId, + }, + }), + retry: false, + onMutate: (lastReadId) => + queryClient.setQueryData(['markers', timeline], (marker) => + marker + ? { + ...marker, + last_read_id: lastReadId, + } + : undefined, + ), + }, + queryClient, + ); }; export { useUpdateMarkerMutation }; diff --git a/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts b/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts index cb224d21a..a300cc73d 100644 --- a/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts +++ b/packages/pl-hooks/lib/hooks/notifications/use-notification-list.ts @@ -1,22 +1,26 @@ import { InfiniteData, useInfiniteQuery, UseInfiniteQueryResult } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { queryClient, usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; -import { deduplicateNotifications } from 'pl-hooks/normalizers/notification'; -import { flattenPages } from 'pl-hooks/utils/queries'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { queryClient, usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; +import { deduplicateNotifications } from '@/normalizers/notification'; +import { flattenPages } from '@/utils/queries'; import type { Notification as BaseNotification, PaginatedResponse, PlApiClient } from 'pl-api'; type UseNotificationParams = { types?: Array; excludeTypes?: Array; -} +}; const getQueryKey = (params: UseNotificationParams) => [ 'notifications', 'lists', - params.types ? params.types.join('|') : params.excludeTypes ? ('exclude:' + params.excludeTypes.join('|')) : 'all', + params.types + ? params.types.join('|') + : params.excludeTypes + ? 'exclude:' + params.excludeTypes.join('|') + : 'all', ]; const importNotifications = (response: PaginatedResponse) => { @@ -33,23 +37,44 @@ const importNotifications = (response: PaginatedResponse) => { }; }; -const useNotificationList = (params: UseNotificationParams): Omit Promise>) | null; - next: (() => Promise>) | null; -}, unknown>, Error>, 'data'> & { data: string[] } => { +const useNotificationList = ( + params: UseNotificationParams, +): Omit< + UseInfiniteQueryResult< + InfiniteData< + { + items: string[]; + previous: (() => Promise>) | null; + next: (() => Promise>) | null; + }, + unknown + >, + Error + >, + 'data' +> & { data: string[] } => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - const notificationsQuery = useInfiniteQuery({ - queryKey: getQueryKey(params), - queryFn: ({ pageParam }) => (pageParam.next ? pageParam.next() : client.notifications.getNotifications({ - types: params.types, - exclude_types: params.excludeTypes, - })).then(importNotifications), - initialPageParam: { previous: null, next: null } as Pick, 'previous' | 'next'>, - getNextPageParam: (response) => response, - }, queryClient); + const notificationsQuery = useInfiniteQuery( + { + queryKey: getQueryKey(params), + queryFn: ({ pageParam }) => + (pageParam.next + ? pageParam.next() + : client.notifications.getNotifications({ + types: params.types, + exclude_types: params.excludeTypes, + }) + ).then(importNotifications), + initialPageParam: { previous: null, next: null } as Pick< + PaginatedResponse, + 'previous' | 'next' + >, + getNextPageParam: (response) => response, + }, + queryClient, + ); const data: string[] = flattenPages(notificationsQuery.data) || []; @@ -62,11 +87,17 @@ const useNotificationList = (params: UseNotificationParams): Omit queryClient.prefetchInfiniteQuery({ queryKey: getQueryKey(params), - queryFn: () => client.notifications.getNotifications({ - types: params.types, - exclude_types: params.excludeTypes, - }).then(importNotifications), - initialPageParam: { previous: null, next: null } as Pick, 'previous' | 'next'>, + queryFn: () => + client.notifications + .getNotifications({ + types: params.types, + exclude_types: params.excludeTypes, + }) + .then(importNotifications), + initialPageParam: { previous: null, next: null } as Pick< + PaginatedResponse, + 'previous' | 'next' + >, }); export { useNotificationList, prefetchNotifications }; diff --git a/packages/pl-hooks/lib/hooks/notifications/use-notification.ts b/packages/pl-hooks/lib/hooks/notifications/use-notification.ts index 54292d587..342327378 100644 --- a/packages/pl-hooks/lib/hooks/notifications/use-notification.ts +++ b/packages/pl-hooks/lib/hooks/notifications/use-notification.ts @@ -1,17 +1,30 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { queryClient, usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { type NormalizedNotification, normalizeNotification } from 'pl-hooks/normalizers/notification'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { queryClient, usePlHooksQueryClient } from '@/contexts/query-client'; +import { type NormalizedNotification, normalizeNotification } from '@/normalizers/notification'; import { useAccount } from '../accounts/use-account'; import { useStatus } from '../statuses/use-status'; -import type { NormalizedAccount as Account } from 'pl-hooks/normalizers/account'; -import type { NormalizedStatus as Status } from 'pl-hooks/normalizers/status'; +import type { NormalizedAccount as Account } from '@/normalizers/account'; +import type { NormalizedStatus as Status } from '@/normalizers/status'; const getNotificationStatusId = (n: NormalizedNotification) => { - if (['mention', 'status', 'reblog', 'favourite', 'poll', 'update', 'emoji_reaction', 'event_reminder', 'participation_accepted', 'participation_request'].includes(n.type)) + if ( + [ + 'mention', + 'status', + 'reblog', + 'favourite', + 'poll', + 'update', + 'emoji_reaction', + 'event_reminder', + 'participation_accepted', + 'participation_request', + ].includes(n.type) + ) // @ts-ignore return n.status_id; return null; @@ -20,7 +33,8 @@ const getNotificationStatusId = (n: NormalizedNotification) => { const importNotification = (notification: NormalizedNotification) => { queryClient.setQueryData( ['notifications', 'entities', notification.id], - existingNotification => existingNotification?.duplicate ? existingNotification : notification, + (existingNotification) => + existingNotification?.duplicate ? existingNotification : notification, ); }; @@ -28,11 +42,14 @@ const useNotification = (notificationId: string) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - const notificationQuery = useQuery({ - queryKey: ['notifications', 'entities', notificationId], - queryFn: () => client.notifications.getNotification(notificationId) - .then(normalizeNotification), - }, queryClient); + const notificationQuery = useQuery( + { + queryKey: ['notifications', 'entities', notificationId], + queryFn: () => + client.notifications.getNotification(notificationId).then(normalizeNotification), + }, + queryClient, + ); const notification = notificationQuery.data; @@ -40,15 +57,19 @@ const useNotification = (notificationId: string) => { queryKey: ['accounts', 'entities', notification?.account_ids], }); - const moveTargetAccountQuery = useAccount(notification?.type === 'move' ? notification.target_id : undefined); + const moveTargetAccountQuery = useAccount( + notification?.type === 'move' ? notification.target_id : undefined, + ); const statusQuery = useStatus(notification ? getNotificationStatusId(notification) : false); - let data: (NormalizedNotification & { - account: Account; - accounts: Array; - target: Account | undefined; - status: Status | undefined; - }) | undefined; + let data: + | (NormalizedNotification & { + account: Account; + accounts: Array; + target: Account | undefined; + status: Status | undefined; + }) + | undefined; if (notification) { data = { diff --git a/packages/pl-hooks/lib/hooks/polls/use-poll.ts b/packages/pl-hooks/lib/hooks/polls/use-poll.ts index 3619344da..cf40180af 100644 --- a/packages/pl-hooks/lib/hooks/polls/use-poll.ts +++ b/packages/pl-hooks/lib/hooks/polls/use-poll.ts @@ -1,17 +1,20 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; const usePoll = (pollId?: string) => { const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useQuery({ - queryKey: ['polls', 'entities', pollId], - queryFn: () => client.polls.getPoll(pollId!), - enabled: !!pollId, - }, queryClient); + return useQuery( + { + queryKey: ['polls', 'entities', pollId], + queryFn: () => client.polls.getPoll(pollId!), + enabled: !!pollId, + }, + queryClient, + ); }; export { usePoll }; diff --git a/packages/pl-hooks/lib/hooks/search/use-search.ts b/packages/pl-hooks/lib/hooks/search/use-search.ts index 95a1674f5..bd5329e80 100644 --- a/packages/pl-hooks/lib/hooks/search/use-search.ts +++ b/packages/pl-hooks/lib/hooks/search/use-search.ts @@ -1,8 +1,8 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; import type { PaginationParams, SearchParams, Tag } from 'pl-api'; @@ -13,21 +13,31 @@ const useSearchAccounts = ( const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useInfiniteQuery({ - queryKey: ['search', 'accounts', query, params], - queryFn: ({ pageParam: offset, signal }) => client.search.search(query!, { - ...params, - offset, - type: 'accounts', - }, { signal }).then(({ accounts }) => { - importEntities({ accounts }); - return accounts.map(({ id }) => id); - }), - enabled: !!query?.trim(), - initialPageParam: 0, - getNextPageParam: (_, allPages) => allPages.flat().length, - select: (data) => data.pages.flat(), - }, queryClient); + return useInfiniteQuery( + { + queryKey: ['search', 'accounts', query, params], + queryFn: ({ pageParam: offset, signal }) => + client.search + .search( + query!, + { + ...params, + offset, + type: 'accounts', + }, + { signal }, + ) + .then(({ accounts }) => { + importEntities({ accounts }); + return accounts.map(({ id }) => id); + }), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data) => data.pages.flat(), + }, + queryClient, + ); }; const useSearchStatuses = ( @@ -37,21 +47,31 @@ const useSearchStatuses = ( const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useInfiniteQuery({ - queryKey: ['search', 'statuses', query, params], - queryFn: ({ pageParam: offset, signal }) => client.search.search(query, { - ...params, - offset, - type: 'statuses', - }, { signal }).then(({ statuses }) => { - importEntities({ statuses }); - return statuses.map(({ id }) => id); - }), - enabled: !!query?.trim(), - initialPageParam: 0, - getNextPageParam: (_, allPages) => allPages.flat().length, - select: (data) => data.pages.flat(), - }, queryClient); + return useInfiniteQuery( + { + queryKey: ['search', 'statuses', query, params], + queryFn: ({ pageParam: offset, signal }) => + client.search + .search( + query, + { + ...params, + offset, + type: 'statuses', + }, + { signal }, + ) + .then(({ statuses }) => { + importEntities({ statuses }); + return statuses.map(({ id }) => id); + }), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data) => data.pages.flat(), + }, + queryClient, + ); }; const useSearchHashtags = ( @@ -61,18 +81,28 @@ const useSearchHashtags = ( const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useInfiniteQuery({ - queryKey: ['search', 'hashtags', query, params], - queryFn: ({ pageParam: offset, signal }) => client.search.search(query, { - ...params, - offset, - type: 'hashtags', - }, { signal }).then(({ hashtags }) => hashtags as Array), - enabled: !!query?.trim(), - initialPageParam: 0, - getNextPageParam: (_, allPages) => allPages.flat().length, - select: (data) => data.pages.flat(), - }, queryClient); + return useInfiniteQuery( + { + queryKey: ['search', 'hashtags', query, params], + queryFn: ({ pageParam: offset, signal }) => + client.search + .search( + query, + { + ...params, + offset, + type: 'hashtags', + }, + { signal }, + ) + .then(({ hashtags }) => hashtags as Array), + enabled: !!query?.trim(), + initialPageParam: 0, + getNextPageParam: (_, allPages) => allPages.flat().length, + select: (data) => data.pages.flat(), + }, + queryClient, + ); }; export { useSearchAccounts, useSearchStatuses, useSearchHashtags }; diff --git a/packages/pl-hooks/lib/hooks/statuses/use-status-history.ts b/packages/pl-hooks/lib/hooks/statuses/use-status-history.ts index dffd96098..cf68a0bb0 100644 --- a/packages/pl-hooks/lib/hooks/statuses/use-status-history.ts +++ b/packages/pl-hooks/lib/hooks/statuses/use-status-history.ts @@ -1,20 +1,29 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { importEntities } from 'pl-hooks/importer'; -import { normalizeStatusEdit } from 'pl-hooks/normalizers/status-edit'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; +import { importEntities } from '@/importer'; +import { normalizeStatusEdit } from '@/normalizers/status-edit'; const useStatusHistory = (statusId: string) => { const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useQuery({ - queryKey: ['statuses', 'history', statusId], - queryFn: () => client.statuses.getStatusHistory(statusId) - .then(history => (importEntities({ accounts: history.map(({ account }) => account) }), history)) - .then(history => history.map(normalizeStatusEdit)), - }, queryClient); + return useQuery( + { + queryKey: ['statuses', 'history', statusId], + queryFn: () => + client.statuses + .getStatusHistory(statusId) + .then( + (history) => ( + importEntities({ accounts: history.map(({ account }) => account) }), history + ), + ) + .then((history) => history.map(normalizeStatusEdit)), + }, + queryClient, + ); }; export { useStatusHistory }; diff --git a/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts b/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts index 1bf126e53..0dc69b538 100644 --- a/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts +++ b/packages/pl-hooks/lib/hooks/statuses/use-status-quotes.ts @@ -1,8 +1,8 @@ import { useInfiniteQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { minifyStatusList } from 'pl-hooks/normalizers/status-list'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; +import { minifyStatusList } from '@/normalizers/status-list'; import type { PaginatedResponse } from 'pl-api'; @@ -10,13 +10,22 @@ const useStatusQuotes = (statusId: string) => { const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - return useInfiniteQuery({ - queryKey: ['statusLists', 'quotes', statusId], - queryFn: ({ pageParam }) => pageParam.next?.() || client.statuses.getStatusQuotes(statusId).then(minifyStatusList), - initialPageParam: { previous: null, next: null, items: [], partial: false } as PaginatedResponse, - getNextPageParam: (page) => page.next ? page : undefined, - select: (data) => data.pages.map(page => page.items).flat(), - }, queryClient); + return useInfiniteQuery( + { + queryKey: ['statusLists', 'quotes', statusId], + queryFn: ({ pageParam }) => + pageParam.next?.() || client.statuses.getStatusQuotes(statusId).then(minifyStatusList), + initialPageParam: { + previous: null, + next: null, + items: [], + partial: false, + } as PaginatedResponse, + getNextPageParam: (page) => (page.next ? page : undefined), + select: (data) => data.pages.map((page) => page.items).flat(), + }, + queryClient, + ); }; export { useStatusQuotes }; diff --git a/packages/pl-hooks/lib/hooks/statuses/use-status-translation.ts b/packages/pl-hooks/lib/hooks/statuses/use-status-translation.ts index 016460c47..ab0d1d30f 100644 --- a/packages/pl-hooks/lib/hooks/statuses/use-status-translation.ts +++ b/packages/pl-hooks/lib/hooks/statuses/use-status-translation.ts @@ -1,7 +1,7 @@ import { useQuery } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { usePlHooksQueryClient } from '@/contexts/query-client'; import type { Translation } from 'pl-api'; @@ -9,12 +9,18 @@ const useStatusTranslation = (statusId: string, targetLanguage?: string) => { const { client } = usePlHooksApiClient(); const queryClient = usePlHooksQueryClient(); - return useQuery({ - queryKey: ['statuses', 'translations', statusId, targetLanguage], - queryFn: () => client.statuses.translateStatus(statusId, targetLanguage) - .then(translation => translation).catch(() => false), - enabled: !!targetLanguage, - }, queryClient); + return useQuery( + { + queryKey: ['statuses', 'translations', statusId, targetLanguage], + queryFn: () => + client.statuses + .translateStatus(statusId, targetLanguage) + .then((translation) => translation) + .catch(() => false), + enabled: !!targetLanguage, + }, + queryClient, + ); }; export { useStatusTranslation }; diff --git a/packages/pl-hooks/lib/hooks/statuses/use-status.ts b/packages/pl-hooks/lib/hooks/statuses/use-status.ts index 85d9ecc2c..eb8241d88 100644 --- a/packages/pl-hooks/lib/hooks/statuses/use-status.ts +++ b/packages/pl-hooks/lib/hooks/statuses/use-status.ts @@ -1,11 +1,11 @@ import { useQueries, useQuery, UseQueryOptions, type UseQueryResult } from '@tanstack/react-query'; -import { usePlHooksApiClient } from 'pl-hooks/contexts/api-client'; -import { queryClient, usePlHooksQueryClient } from 'pl-hooks/contexts/query-client'; -import { usePoll } from 'pl-hooks/hooks/polls/use-poll'; -import { importEntities } from 'pl-hooks/importer'; -import { type NormalizedAccount } from 'pl-hooks/normalizers/account'; -import { type NormalizedStatus, normalizeStatus } from 'pl-hooks/normalizers/status'; +import { usePlHooksApiClient } from '@/contexts/api-client'; +import { queryClient, usePlHooksQueryClient } from '@/contexts/query-client'; +import { usePoll } from '@/hooks/polls/use-poll'; +import { importEntities } from '@/importer'; +import { type NormalizedAccount } from '@/normalizers/account'; +import { type NormalizedStatus, normalizeStatus } from '@/normalizers/status'; import type { Poll } from 'pl-api'; @@ -80,10 +80,7 @@ import type { Poll } from 'pl-api'; // }, [])), []); const importStatus = (status: NormalizedStatus) => { - queryClient.setQueryData( - ['statuses', 'entities', status.id], - status, - ); + queryClient.setQueryData(['statuses', 'entities', status.id], status); }; type Status = NormalizedStatus & { @@ -98,21 +95,33 @@ interface UseStatusOpts { withReblog?: boolean; } -type UseStatusQueryResult = Omit, 'data'> & { data: Status | undefined }; +type UseStatusQueryResult = Omit, 'data'> & { + data: Status | undefined; +}; -const useStatus = (statusId?: string, opts: UseStatusOpts = { withReblog: true }): UseStatusQueryResult => { +const useStatus = ( + statusId?: string, + opts: UseStatusOpts = { withReblog: true }, +): UseStatusQueryResult => { const queryClient = usePlHooksQueryClient(); const { client } = usePlHooksApiClient(); - const statusQuery = useQuery({ - queryKey: ['statuses', 'entities', statusId], - queryFn: () => client.statuses.getStatus(statusId!, { - language: opts.language, - }) - .then(status => (importEntities({ statuses: [status] }, { withParents: false }), status)) - .then(normalizeStatus), - enabled: !!statusId, - }, queryClient); + const statusQuery = useQuery( + { + queryKey: ['statuses', 'entities', statusId], + queryFn: () => + client.statuses + .getStatus(statusId!, { + language: opts.language, + }) + .then( + (status) => (importEntities({ statuses: [status] }, { withParents: false }), status), + ) + .then(normalizeStatus), + enabled: !!statusId, + }, + queryClient, + ); const status = statusQuery.data; @@ -123,11 +132,15 @@ const useStatus = (statusId?: string, opts: UseStatusOpts = { withReblog: true } reblogQuery = useStatus(status?.reblog_id || undefined, { ...opts, withReblog: false }); } - const accountsQuery = useQueries[]>({ - queries: status?.account_ids.map(accountId => ({ - queryKey: ['accounts', 'entities', accountId], - })) || [], - }, queryClient); + const accountsQuery = useQueries[]>( + { + queries: + status?.account_ids.map((accountId) => ({ + queryKey: ['accounts', 'entities', accountId], + })) || [], + }, + queryClient, + ); let data: Status | undefined; diff --git a/packages/pl-hooks/lib/importer.ts b/packages/pl-hooks/lib/importer.ts index 19a735d7e..836e5d9fa 100644 --- a/packages/pl-hooks/lib/importer.ts +++ b/packages/pl-hooks/lib/importer.ts @@ -1,6 +1,11 @@ -import { queryClient } from 'pl-hooks/contexts/query-client'; +import { queryClient } from '@/contexts/query-client'; -import { type DeduplicatedNotification, getNotificationStatus, type NormalizedNotification, normalizeNotification } from './normalizers/notification'; +import { + type DeduplicatedNotification, + getNotificationStatus, + type NormalizedNotification, + normalizeNotification, +} from './normalizers/notification'; import { type NormalizedStatus, normalizeStatus } from './normalizers/status'; import type { @@ -11,45 +16,51 @@ import type { Status as BaseStatus, } from 'pl-api'; -const importAccount = (account: BaseAccount) => queryClient.setQueryData( - ['accounts', 'entities', account.id], account, -); +const importAccount = (account: BaseAccount) => + queryClient.setQueryData(['accounts', 'entities', account.id], account); -const importGroup = (group: BaseGroup) => queryClient.setQueryData( - ['groups', 'entities', group.id], group, -); +const importGroup = (group: BaseGroup) => + queryClient.setQueryData(['groups', 'entities', group.id], group); -const importNotification = (notification: DeduplicatedNotification) => queryClient.setQueryData( - ['notifications', 'entities', notification.id], - existingNotification => existingNotification?.duplicate ? existingNotification : normalizeNotification(notification), -); +const importNotification = (notification: DeduplicatedNotification) => + queryClient.setQueryData( + ['notifications', 'entities', notification.id], + (existingNotification) => + existingNotification?.duplicate ? existingNotification : normalizeNotification(notification), + ); -const importPoll = (poll: BasePoll) => queryClient.setQueryData( - ['polls', 'entities', poll.id], poll, -); +const importPoll = (poll: BasePoll) => + queryClient.setQueryData(['polls', 'entities', poll.id], poll); -const importRelationship = (relationship: BaseRelationship) => queryClient.setQueryData( - ['relationships', 'entities', relationship.id], relationship, -); +const importRelationship = (relationship: BaseRelationship) => + queryClient.setQueryData( + ['relationships', 'entities', relationship.id], + relationship, + ); -const importStatus = (status: BaseStatus) => queryClient.setQueryData( - ['statuses', 'entities', status.id], normalizeStatus(status), -); +const importStatus = (status: BaseStatus) => + queryClient.setQueryData( + ['statuses', 'entities', status.id], + normalizeStatus(status), + ); -const isEmpty = (object: Record) => !Object.values(object).some(value => value); +const isEmpty = (object: Record) => !Object.values(object).some((value) => value); type OptionalArray = Array; -const importEntities = (entities: { - accounts?: OptionalArray; - groups?: OptionalArray; - notifications?: OptionalArray; - polls?: OptionalArray; - statuses?: OptionalArray; - relationships?: OptionalArray; -}, options = { - withParents: true, -}) => { +const importEntities = ( + entities: { + accounts?: OptionalArray; + groups?: OptionalArray; + notifications?: OptionalArray; + polls?: OptionalArray; + statuses?: OptionalArray; + relationships?: OptionalArray; + }, + options = { + withParents: true, + }, +) => { const accounts: Record = {}; const groups: Record = {}; const notifications: Record = {}; @@ -60,7 +71,10 @@ const importEntities = (entities: { const processAccount = (account: BaseAccount, withSelf = true) => { if (withSelf) accounts[account.id] = account; - queryClient.setQueryData(['accounts', 'byAcct', account.acct.toLocaleLowerCase()], account.id); + queryClient.setQueryData( + ['accounts', 'byAcct', account.acct.toLocaleLowerCase()], + account.id, + ); if (account.moved) processAccount(account.moved); if (account.relationship) relationships[account.relationship.id] = account.relationship; @@ -85,20 +99,25 @@ const importEntities = (entities: { processAccount(status.account); } - if (status.quote && 'quoted_status' in status.quote && status.quote.quoted_status) processStatus(status.quote.quoted_status); + if (status.quote && 'quoted_status' in status.quote && status.quote.quoted_status) + processStatus(status.quote.quoted_status); if (status.reblog) processStatus(status.reblog); if (status.poll) polls[status.poll.id] = status.poll; if (status.group) groups[status.group.id] = status.group; }; if (options.withParents) { - entities.groups?.forEach(group => group && (groups[group.id] = group)); - entities.polls?.forEach(poll => poll && (polls[poll.id] = poll)); - entities.relationships?.forEach(relationship => relationship && (relationships[relationship.id] = relationship)); + entities.groups?.forEach((group) => group && (groups[group.id] = group)); + entities.polls?.forEach((poll) => poll && (polls[poll.id] = poll)); + entities.relationships?.forEach( + (relationship) => relationship && (relationships[relationship.id] = relationship), + ); } entities.accounts?.forEach((account) => account && processAccount(account, options.withParents)); - entities.notifications?.forEach((notification) => notification && processNotification(notification, options.withParents)); + entities.notifications?.forEach( + (notification) => notification && processNotification(notification, options.withParents), + ); entities.statuses?.forEach((status) => status && processStatus(status, options.withParents)); if (!isEmpty(accounts)) Object.values(accounts).forEach(importAccount); diff --git a/packages/pl-hooks/lib/normalizers/notification.ts b/packages/pl-hooks/lib/normalizers/notification.ts index b9dfddb2b..33889b9f1 100644 --- a/packages/pl-hooks/lib/normalizers/notification.ts +++ b/packages/pl-hooks/lib/normalizers/notification.ts @@ -1,11 +1,16 @@ import omit from 'lodash/omit'; -import type { AccountWarning, Account as BaseAccount, Notification as BaseNotification, RelationshipSeveranceEvent } from 'pl-api'; +import type { + AccountWarning, + Account as BaseAccount, + Notification as BaseNotification, + RelationshipSeveranceEvent, +} from 'pl-api'; type DeduplicatedNotification = BaseNotification & { accounts: Array; duplicate?: boolean; -} +}; const STATUS_NOTIFICATION_TYPES = [ 'mention', @@ -27,26 +32,42 @@ const getNotificationStatus = (n: Pick) => { return null; }; -const deduplicateNotifications = (notifications: Array): Array => { +const deduplicateNotifications = ( + notifications: Array, +): Array => { const deduplicatedNotifications: DeduplicatedNotification[] = []; for (const notification of notifications) { if (STATUS_NOTIFICATION_TYPES.includes(notification.type)) { - const existingNotification = deduplicatedNotifications - .find(deduplicated => - deduplicated.type === notification.type - && ((notification.type === 'emoji_reaction' && deduplicated.type === 'emoji_reaction') ? notification.emoji === deduplicated.emoji : true) - && getNotificationStatus(deduplicated)?.id === getNotificationStatus(notification)?.id, - ); + const existingNotification = deduplicatedNotifications.find( + (deduplicated) => + deduplicated.type === notification.type && + (notification.type === 'emoji_reaction' && deduplicated.type === 'emoji_reaction' + ? notification.emoji === deduplicated.emoji + : true) && + getNotificationStatus(deduplicated)?.id === getNotificationStatus(notification)?.id, + ); if (existingNotification) { existingNotification.accounts.push(notification.account); - deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: true }); + deduplicatedNotifications.push({ + ...notification, + accounts: [notification.account], + duplicate: true, + }); } else { - deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false }); + deduplicatedNotifications.push({ + ...notification, + accounts: [notification.account], + duplicate: false, + }); } } else { - deduplicatedNotifications.push({ ...notification, accounts: [notification.account], duplicate: false }); + deduplicatedNotifications.push({ + ...notification, + accounts: [notification.account], + duplicate: false, + }); } } @@ -65,50 +86,61 @@ const normalizeNotification = (notification: BaseNotification | DeduplicatedNoti } & ( | { type: 'follow' | 'follow_request' | 'admin.sign_up' | 'bite' } | { - type: 'mention'; - subtype?: 'reply'; - status_id: string; - } + type: 'mention'; + subtype?: 'reply'; + status_id: string; + } | { - type: 'status' | 'reblog' | 'favourite' | 'poll' | 'update' | 'event_reminder' | 'quote' | 'quoted_update'; - status_id: string; - } + type: + | 'status' + | 'reblog' + | 'favourite' + | 'poll' + | 'update' + | 'event_reminder' + | 'quote' + | 'quoted_update'; + status_id: string; + } | { - type: 'admin.report'; - report: Report; - } + type: 'admin.report'; + report: Report; + } | { - type: 'severed_relationships'; - relationship_severance_event: RelationshipSeveranceEvent; - } + type: 'severed_relationships'; + relationship_severance_event: RelationshipSeveranceEvent; + } | { - type: 'moderation_warning'; - moderation_warning: AccountWarning; - } + type: 'moderation_warning'; + moderation_warning: AccountWarning; + } | { - type: 'move'; - target_id: string; - } + type: 'move'; + target_id: string; + } | { - type: 'emoji_reaction'; - emoji: string; - emoji_url: string | null; - status_id: string; - } + type: 'emoji_reaction'; + emoji: string; + emoji_url: string | null; + status_id: string; + } | { - type: 'chat_mention'; - chat_message_id: string; - } + type: 'chat_mention'; + chat_message_id: string; + } | { - type: 'participation_accepted' | 'participation_request'; - status_id: string; - participation_message: string | null; - } + type: 'participation_accepted' | 'participation_request'; + status_id: string; + participation_message: string | null; + } ) = { duplicate: false, ...omit(notification, ['account', 'accounts', 'status', 'target', 'chat_message']), account_id: notification.account.id, - account_ids: ('accounts' in notification) ? notification.accounts.map(({ id }) => id) : [notification.account.id], + account_ids: + 'accounts' in notification + ? notification.accounts.map(({ id }) => id) + : [notification.account.id], created_at: notification.created_at, id: notification.id, type: notification.type, @@ -119,11 +151,19 @@ const normalizeNotification = (notification: BaseNotification | DeduplicatedNoti // @ts-ignore if (notification.target) minifiedNotification.target_id = notification.target.id; // @ts-ignore - if (notification.chat_message) minifiedNotification.chat_message_id = notification.chat_message.id; + if (notification.chat_message) + // @ts-ignore + minifiedNotification.chat_message_id = notification.chat_message.id; return minifiedNotification; }; type NormalizedNotification = ReturnType; -export { deduplicateNotifications, getNotificationStatus, normalizeNotification, type DeduplicatedNotification, type NormalizedNotification }; +export { + deduplicateNotifications, + getNotificationStatus, + normalizeNotification, + type DeduplicatedNotification, + type NormalizedNotification, +}; diff --git a/packages/pl-hooks/lib/normalizers/status-list.ts b/packages/pl-hooks/lib/normalizers/status-list.ts index 40b4a94d9..94dbd950d 100644 --- a/packages/pl-hooks/lib/normalizers/status-list.ts +++ b/packages/pl-hooks/lib/normalizers/status-list.ts @@ -1,14 +1,19 @@ import { PaginatedResponse, Status } from 'pl-api'; -import { importEntities } from 'pl-hooks/importer'; +import { importEntities } from '@/importer'; -const minifyStatusList = ({ previous, next, items, ...response }: PaginatedResponse): PaginatedResponse => { +const minifyStatusList = ({ + previous, + next, + items, + ...response +}: PaginatedResponse): PaginatedResponse => { importEntities({ statuses: items }); return { ...response, previous: previous ? () => previous().then(minifyStatusList) : null, next: next ? () => next().then(minifyStatusList) : null, - items: items.map(status => status.id), + items: items.map((status) => status.id), }; }; diff --git a/packages/pl-hooks/lib/normalizers/status.ts b/packages/pl-hooks/lib/normalizers/status.ts index 767a9f9de..187836cdd 100644 --- a/packages/pl-hooks/lib/normalizers/status.ts +++ b/packages/pl-hooks/lib/normalizers/status.ts @@ -1,10 +1,30 @@ -import { type Account as BaseAccount, type Status as BaseStatus, type MediaAttachment, mentionSchema } from 'pl-api'; +import { + type Account as BaseAccount, + type Status as BaseStatus, + type MediaAttachment, + mentionSchema, +} from 'pl-api'; import * as v from 'valibot'; type StatusApprovalStatus = Exclude; -type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct' | 'group' | 'mutuals_only' | 'local'; +type StatusVisibility = + | 'public' + | 'unlisted' + | 'private' + | 'direct' + | 'group' + | 'mutuals_only' + | 'local'; -const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...status }: BaseStatus & { accounts?: Array }) => { +const normalizeStatus = ({ + account, + accounts, + reblog, + poll, + group, + quote, + ...status +}: BaseStatus & { accounts?: Array }) => { // Sort the replied-to mention to the top let mentions = status.mentions.toSorted((a, _b) => { if (a.id === status.in_reply_to_account_id) { @@ -16,7 +36,7 @@ const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...sta // Add self to mentions if it's a reply to self const isSelfReply = account.id === status.in_reply_to_account_id; - const hasSelfMention = status.mentions.some(mention => account.id === mention.id); + const hasSelfMention = status.mentions.some((mention) => account.id === mention.id); if (isSelfReply && !hasSelfMention) { const selfMention = v.parse(mentionSchema, account); @@ -24,10 +44,11 @@ const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...sta } // Normalize event - let event: BaseStatus['event'] & ({ - banner: MediaAttachment | null; - links: Array; - } | null) = null; + let event: BaseStatus['event'] & + ({ + banner: MediaAttachment | null; + links: Array; + } | null) = null; let media_attachments = status.media_attachments; if (status.event) { @@ -39,8 +60,10 @@ const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...sta media_attachments = media_attachments.slice(1); } - const links = media_attachments.filter(attachment => attachment.mime_type === 'text/html'); - media_attachments = media_attachments.filter(attachment => attachment.mime_type !== 'text/html'); + const links = media_attachments.filter((attachment) => attachment.mime_type === 'text/html'); + media_attachments = media_attachments.filter( + (attachment) => attachment.mime_type !== 'text/html', + ); event = { ...status.event, @@ -51,7 +74,7 @@ const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...sta return { account_id: account.id, - account_ids: accounts?.map(account => account.id) || [account.id], + account_ids: accounts?.map((account) => account.id) || [account.id], reblog_id: reblog?.id || null, poll_id: poll?.id || null, group_id: group?.id || null, @@ -59,7 +82,7 @@ const normalizeStatus = ({ account, accounts, reblog, poll, group, quote, ...sta showFiltered: null as null | boolean, ...status, mentions, - filtered: status.filtered?.map(result => result.filter.title), + filtered: status.filtered?.map((result) => result.filter.title), event, media_attachments, }; diff --git a/packages/pl-hooks/lib/utils/queries.ts b/packages/pl-hooks/lib/utils/queries.ts index 80b3e262b..07fa8a543 100644 --- a/packages/pl-hooks/lib/utils/queries.ts +++ b/packages/pl-hooks/lib/utils/queries.ts @@ -2,11 +2,10 @@ import type { InfiniteData } from '@tanstack/react-query'; import type { PaginatedResponse } from 'pl-api'; /** Flatten paginated results into a single array. */ -const flattenPages = (queryData: InfiniteData, 'items'>> | undefined) => { - return queryData?.pages.reduce( - (prev: T[], curr) => [...prev, ...(curr.items)], - [], - ); +const flattenPages = ( + queryData: InfiniteData, 'items'>> | undefined, +) => { + return queryData?.pages.reduce((prev: T[], curr) => [...prev, ...curr.items], []); }; export { flattenPages }; diff --git a/packages/pl-hooks/package.json b/packages/pl-hooks/package.json index 29e898a48..9f574bb98 100644 --- a/packages/pl-hooks/package.json +++ b/packages/pl-hooks/package.json @@ -1,47 +1,51 @@ { "name": "pl-hooks", "version": "0.0.14", - "type": "module", "homepage": "https://codeberg.org/mkljczk/nicolium/src/branch/develop/packages/pl-hooks", + "bugs": { + "url": "https://codeberg.org/mkljczk/nicolium/issues" + }, + "license": "AGPL-3.0-or-later", "repository": { "type": "git", "url": "https://codeberg.org/mkljczk/nicolium" }, - "bugs": { - "url": "https://codeberg.org/mkljczk/nicolium/issues" - }, + "files": [ + "dist" + ], + "type": "module", + "module": "./dist/main.es.js", + "types": "dist/main.d.ts", "scripts": { "dev": "vite", "build": "tsc --p ./tsconfig-build.json && vite build", "preview": "vite preview", - "lint": "npx eslint --ext .js,.jsx,.cjs,.mjs,.ts,.tsx . --cache" - }, - "license": "AGPL-3.0-or-later", - "devDependencies": { - "@types/lodash": "^4.17.10", - "@types/node": "^20.14.12", - "@types/react": "^18.3.11", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.49.0", - "eslint-import-resolver-typescript": "^3.6.3", - "eslint-plugin-compat": "^6.0.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-promise": "^6.0.0", - "typescript": "^5.6.2", - "vite": "^8.0.0-beta.14", - "vite-plugin-dts": "^4.2.1" + "lint": "oxlint", + "fmt": "oxfmt", + "fmt:check": "oxfmt --check" }, "dependencies": { - "@tanstack/react-query": "^5.84.1", + "@tanstack/react-query": "^5.90.21", "lodash": "^4.17.23", "pl-api": "workspace:*", - "react": "^18.3.1", + "react": "^19.2.4", "valibot": "^1.2.0" }, - "module": "./dist/main.es.js", - "types": "dist/main.d.ts", - "files": [ - "dist" - ] + "devDependencies": { + "@types/lodash": "^4.17.24", + "@types/node": "^25.3.0", + "@types/react": "^19.2.14", + "oxfmt": "^0.35.0", + "oxlint": "^1.50.0", + "oxlint-tsgolint": "^0.14.2", + "typescript": "^5.9.3", + "vite": "^7.3.1", + "vite-plugin-dts": "^4.5.4ls" + }, + "lint-staged": { + "*.{js,cjs,mjs,ts,tsx}": [ + "oxlint", + "oxfmt --check" + ] + } } diff --git a/packages/pl-hooks/tsconfig-build.json b/packages/pl-hooks/tsconfig-build.json index 160480b19..e0a84f7e8 100644 --- a/packages/pl-hooks/tsconfig-build.json +++ b/packages/pl-hooks/tsconfig-build.json @@ -1,4 +1,4 @@ { - "extends": "./tsconfig.json", - "include": ["lib"] - } \ No newline at end of file + "extends": "./tsconfig.json", + "include": ["lib"] +} diff --git a/packages/pl-hooks/tsconfig.json b/packages/pl-hooks/tsconfig.json index d3bd13331..c64cfea4b 100644 --- a/packages/pl-hooks/tsconfig.json +++ b/packages/pl-hooks/tsconfig.json @@ -22,8 +22,8 @@ "noFallthroughCasesInSwitch": true, "paths": { - "pl-hooks/*": ["lib/*"], - }, + "@/*": ["lib/*"] + } }, "include": ["lib"] } diff --git a/packages/pl-hooks/vite.config.ts b/packages/pl-hooks/vite.config.ts index 1b9f65735..16fa37d1b 100644 --- a/packages/pl-hooks/vite.config.ts +++ b/packages/pl-hooks/vite.config.ts @@ -23,9 +23,7 @@ export default defineConfig({ }, }, resolve: { - alias: [ - { find: 'pl-hooks', replacement: fileURLToPath(new URL('./lib', import.meta.url)) }, - ], + alias: [{ find: '@/', replacement: fileURLToPath(new URL('./lib/', import.meta.url)) }], dedupe: ['valibot'], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acc9be920..f8cd87475 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,11 +5,9 @@ settings: excludeLinksFromLockfile: false overrides: - '@types/react': ^18.3.18 - '@types/react-dom': ^18.3.5 - glob-parent: ^6.0.1 - jsonwebtoken: ^9.0.0 - loader-utils: ^2.0.3 + '@types/react': ^19.2.14 + '@types/react-dom': ^19.2.3 + glob-parent: ^6.0.2 importers: @@ -19,8 +17,8 @@ importers: specifier: ^9.0.0 version: 9.1.7 lint-staged: - specifier: '>=10' - version: 15.5.2 + specifier: ^16.2.7 + version: 16.2.7 packages/pl-api: dependencies: @@ -150,7 +148,7 @@ importers: version: 10.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@reduxjs/toolkit': specifier: ^2.5.0 - version: 2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1))(react@19.2.4) + version: 2.8.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) '@sentry/browser': specifier: ^8.47.0 version: 8.55.0 @@ -249,7 +247,7 @@ importers: version: 2.6.0 html-react-parser: specifier: ^5.2.17 - version: 5.2.17(@types/react@18.3.27)(react@19.2.4) + version: 5.2.17(@types/react@19.2.14)(react@19.2.4) intersection-observer: specifier: ^0.12.2 version: 0.12.2 @@ -330,10 +328,10 @@ importers: version: 4.2.0(react@19.2.4) react-intl: specifier: ^8.1.3 - version: 8.1.3(@types/react@18.3.27)(react@19.2.4)(typescript@5.9.3) + version: 8.1.3(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3) react-redux: specifier: ^9.0.4 - version: 9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1) + version: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) react-sparklines: specifier: ^1.7.0 version: 1.7.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -369,7 +367,7 @@ importers: version: 6.4.0 use-mutative: specifier: ^1.3.1 - version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4) + version: 1.3.1(@types/react@19.2.14)(mutative@1.3.0)(react@19.2.4) util: specifier: ^0.12.5 version: 0.12.5 @@ -378,10 +376,10 @@ importers: version: 1.2.0(typescript@5.9.3) zustand: specifier: ^5.0.11 - version: 5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + version: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) zustand-mutative: specifier: ^1.3.1 - version: 1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) + version: 1.3.1(@types/react@19.2.14)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) devDependencies: '@formatjs/cli': specifier: ^6.13.0 @@ -402,14 +400,14 @@ importers: specifier: ^1.0.3 version: 1.0.3 '@types/react': - specifier: ^18.3.18 - version: 18.3.27 + specifier: ^19.2.14 + version: 19.2.14 '@types/react-color': specifier: ^3.0.13 - version: 3.0.13(@types/react@18.3.27) + version: 3.0.13(@types/react@19.2.14) '@types/react-dom': - specifier: ^18.3.5 - version: 18.3.7(@types/react@18.3.27) + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) '@types/react-sparklines': specifier: ^1.7.5 version: 1.7.5 @@ -480,8 +478,8 @@ importers: packages/pl-hooks: dependencies: '@tanstack/react-query': - specifier: ^5.84.1 - version: 5.84.1(react@18.3.1) + specifier: ^5.90.21 + version: 5.90.21(react@19.2.4) lodash: specifier: ^4.17.23 version: 4.17.23 @@ -489,51 +487,39 @@ importers: specifier: workspace:* version: link:../pl-api react: - specifier: ^18.3.1 - version: 18.3.1 + specifier: ^19.2.4 + version: 19.2.4 valibot: specifier: ^1.2.0 - version: 1.2.0(typescript@5.9.2) + version: 1.2.0(typescript@5.9.3) devDependencies: '@types/lodash': - specifier: ^4.17.10 - version: 4.17.20 + specifier: ^4.17.24 + version: 4.17.24 '@types/node': - specifier: ^20.14.12 - version: 20.19.9 + specifier: ^25.3.0 + version: 25.3.0 '@types/react': - specifier: ^18.3.18 - version: 18.3.23 - '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) - '@typescript-eslint/parser': - specifier: ^7.0.0 - version: 7.18.0(eslint@8.57.1)(typescript@5.9.2) - eslint: - specifier: ^8.49.0 - version: 8.57.1 - eslint-import-resolver-typescript: - specifier: ^3.6.3 - version: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - eslint-plugin-compat: - specifier: ^6.0.1 - version: 6.0.2(eslint@8.57.1) - eslint-plugin-import: - specifier: ^2.28.1 - version: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) - eslint-plugin-promise: - specifier: ^6.0.0 - version: 6.6.0(eslint@8.57.1) + specifier: ^19.2.14 + version: 19.2.14 + oxfmt: + specifier: ^0.35.0 + version: 0.35.0 + oxlint: + specifier: ^1.50.0 + version: 1.50.0(oxlint-tsgolint@0.14.2) + oxlint-tsgolint: + specifier: ^0.14.2 + version: 0.14.2 typescript: - specifier: ^5.6.2 - version: 5.9.2 + specifier: ^5.9.3 + version: 5.9.3 vite: - specifier: ^8.0.0-beta.14 - version: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) + specifier: ^7.3.1 + version: 7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) vite-plugin-dts: - specifier: ^4.2.1 - version: 4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) + specifier: ^4.5.4ls + version: 4.5.4(@types/node@25.3.0)(rollup@4.57.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)) packages: @@ -1200,21 +1186,12 @@ packages: '@dual-bundle/import-meta-resolve@4.1.0': resolution: {integrity: sha512-+nxncfwHM5SgAtrVzgpzJOI1ol0PkumhVo469KCf9lUi21IGcY90G98VuHm9VRrUypmAzawAHO9bs6hqeADaVg==} - '@emnapi/core@1.4.5': - resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} - '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} - '@emnapi/runtime@1.4.5': - resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} - '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} - '@emnapi/wasi-threads@1.0.4': - resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} - '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} @@ -1791,9 +1768,6 @@ packages: '@material/material-color-utilities@0.3.0': resolution: {integrity: sha512-ztmtTd6xwnuh2/xu+Vb01btgV8SQWYCaK56CkRK8gEkWe5TuDyBcYJ0wgkMRn+2VcE9KUmhvkz+N9GHrqw/C0g==} - '@mdn/browser-compat-data@5.7.6': - resolution: {integrity: sha512-7xdrMX0Wk7grrTZQwAoy1GkvPMFoizStUoL+VmtUkAxegbCCec+3FKwOM6yc/uGU5+BEczQHXAlWiqvM8JeENg==} - '@microsoft/api-extractor-model@7.30.7': resolution: {integrity: sha512-TBbmSI2/BHpfR9YhQA7nH0nqVmGgJ0xH0Ex4D99/qBDAUpnhA2oikGmdXanbw9AWWY/ExBYIpkmY8dBHdla3YQ==} @@ -1810,9 +1784,6 @@ packages: '@mkljczk/url-purify@0.0.5': resolution: {integrity: sha512-Ejrr9xiiZNkjT9hRmLfqkpL5uU/0Loo5tzh24wJXT3CDevccINnbmrvxQyW8DU5iSKfetWkYWvFHP5ntREw9VQ==} - '@napi-rs/wasm-runtime@0.2.12': - resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} @@ -1828,14 +1799,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - - '@oxc-project/runtime@0.113.0': - resolution: {integrity: sha512-apRWH/gXeAsl/sQiblIZnLu7f8P/C9S2fJIicuHV9KOK9J7Hv1JPyTwB8WAcOrDBfjs+cbzjMOGe9UR2ue4ZQg==} - engines: {node: ^20.19.0 || >=22.12.0} - '@oxc-project/types@0.113.0': resolution: {integrity: sha512-Tp3XmgxwNQ9pEN9vxgJBAqdRamHibi76iowQ38O2I4PMpcvNRQNVsU2n1x1nv9yh0XoTrGFzf7cZSGxmixxrhA==} @@ -2553,9 +2516,6 @@ packages: cpu: [x64] os: [win32] - '@rtsao/scc@1.1.0': - resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/node-core-library@5.14.0': resolution: {integrity: sha512-eRong84/rwQUlATGFW3TMTYVyqL1vfW9Lf10PH+mVGfIb9HzU3h5AASNIw+axnBLjnD0n3rT5uQBwu9fvzATrg==} peerDependencies: @@ -2663,9 +2623,6 @@ packages: resolution: {integrity: sha512-MRXCiG8IcjrN/3LGu7Wy6lKZkbwOb5YelOBYtHxnxKYj2WlO2FrqASILSiJcwdES5Sz2QJEIeuvO5JY8cKaGQw==} engines: {node: '>=18'} - '@tanstack/query-core@5.83.1': - resolution: {integrity: sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q==} - '@tanstack/query-core@5.90.20': resolution: {integrity: sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==} @@ -2676,11 +2633,6 @@ packages: react: '>=16.8' react-dom: '>=16.8' - '@tanstack/react-query@5.84.1': - resolution: {integrity: sha512-zo7EUygcWJMQfFNWDSG7CBhy8irje/XY0RDVKKV4IQJAysb+ZJkkJPcnQi+KboyGUgT+SQebRFoTqLuTtfoDLw==} - peerDependencies: - react: ^18 || ^19 - '@tanstack/react-query@5.90.21': resolution: {integrity: sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==} peerDependencies: @@ -2726,9 +2678,6 @@ packages: '@twemoji/svg@15.0.0': resolution: {integrity: sha512-ZSPef2B6nBaYnfgdTbAy4jgW95o7pi2xPGwGCU+WMTxo7J6B1lMPTWwSq/wTuiMq+N0khQ90CcvYp1wFoQpo/w==} - '@tybys/wasm-util@0.10.0': - resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -2774,7 +2723,7 @@ packages: '@types/hoist-non-react-statics@3.3.7': resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 '@types/http-link-header@1.0.7': resolution: {integrity: sha512-snm5oLckop0K3cTDAiBnZDy6ncx9DJ3mCRDvs42C884MbVYPP74Tiq2hFsSDRTyjK6RyDYDIulPiW23ge+g5Lw==} @@ -2782,21 +2731,12 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - '@types/leaflet@1.9.21': resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - '@types/lodash@4.17.24': resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} - '@types/node@20.19.9': - resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==} - '@types/node@22.19.11': resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} @@ -2809,18 +2749,15 @@ packages: '@types/picomatch@4.0.2': resolution: {integrity: sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA==} - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - '@types/react-color@3.0.13': resolution: {integrity: sha512-2c/9FZ4ixC5T3JzN0LP5Cke2Mf0MKOP2Eh0NPDPWmuVH3NjPyhEjqNMQpN1Phr5m74egAy+p2lYNAFrX1z9Yrg==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 - '@types/react-dom@18.3.7': - resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 '@types/react-sparklines@1.7.5': resolution: {integrity: sha512-rIAmNyRKUqWWnaQMjNrxMNkgEFi5f9PrdczSNxj5DscAa48y4i9P0fRKZ72FmNcFsdg6Jx4o6CXWZtIaC0OJOg==} @@ -2828,16 +2765,13 @@ packages: '@types/react-swipeable-views@0.13.6': resolution: {integrity: sha512-Pe9TxRRo098Lxi59YQ8KWVuyBOvt9nJK5AtAqGiRsnl+6PXLknuiLhg0+mGuM9Bn+nun+Ed65Kb1Yl171vCdyA==} - '@types/react@18.3.23': - resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} - - '@types/react@18.3.27': - resolution: {integrity: sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==} + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} '@types/reactcss@1.2.13': resolution: {integrity: sha512-gi3S+aUi6kpkF5vdhUsnkwbiSEIU/BEJyD7kBy2SudWBUuKmJk8AQKE0OVcQQeEy40Azh0lV6uynxlikYIJuwg==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2854,64 +2788,6 @@ packages: '@types/use-sync-external-store@0.0.6': resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 - - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} - '@uidotdev/usehooks@2.4.1': resolution: {integrity: sha512-1I+RwWyS+kdv3Mv0Vmc+p0dPYH0DTRAo04HLyXReYBL9AeseDWUJyi4THuksBJcu9F0Pih69Ak150VDnqbVnXg==} engines: {node: '>=16'} @@ -2925,101 +2801,6 @@ packages: '@unicode/unicode-17.0.0@1.6.16': resolution: {integrity: sha512-advq5p36zZ+PDRUpDkWcHHR++R19kx0LYB5iG3bj0KB8mYVKg0ywS996e2bXeXxDb8XdOF7KTivcx7VkYie1pg==} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} - cpu: [arm] - os: [android] - - '@unrs/resolver-binding-android-arm64@1.11.1': - resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} - cpu: [arm64] - os: [android] - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} - cpu: [arm64] - os: [darwin] - - '@unrs/resolver-binding-darwin-x64@1.11.1': - resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} - cpu: [x64] - os: [darwin] - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} - cpu: [x64] - os: [freebsd] - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} - cpu: [arm] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} - cpu: [arm64] - os: [linux] - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} - cpu: [ppc64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} - cpu: [riscv64] - os: [linux] - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} - cpu: [s390x] - os: [linux] - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} - cpu: [x64] - os: [linux] - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} - engines: {node: '>=14.0.0'} - cpu: [wasm32] - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} - cpu: [arm64] - os: [win32] - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} - cpu: [ia32] - os: [win32] - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} - cpu: [x64] - os: [win32] - '@use-gesture/core@10.3.1': resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} @@ -3236,33 +3017,14 @@ packages: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.6: - resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - arraybuffer.prototype.slice@1.0.4: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} - ast-metadata-inferer@0.8.1: - resolution: {integrity: sha512-ht3Dm6Zr7SXv6t1Ra6gFo0+kLDglHGrEbYihTkcycrbHw7WCcuhBzPlJYHEsIpycaUwzsJHje+vUcxXUX4ztTA==} - astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -3393,9 +3155,6 @@ packages: caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} - caniuse-lite@1.0.30001731: - resolution: {integrity: sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==} - caniuse-lite@1.0.30001762: resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==} @@ -3406,10 +3165,6 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chalk@5.5.0: - resolution: {integrity: sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==} - engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} - char-regex@1.0.2: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} @@ -3434,9 +3189,9 @@ packages: resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} engines: {node: '>=18'} - cli-truncate@4.0.0: - resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} - engines: {node: '>=18'} + cli-truncate@5.1.1: + resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==} + engines: {node: '>=20'} clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} @@ -3458,9 +3213,9 @@ packages: colorjs.io@0.5.2: resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} - commander@13.1.0: - resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} - engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3626,14 +3381,6 @@ packages: de-indent@1.0.2: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.1: resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} engines: {node: '>=6.0'} @@ -3702,10 +3449,6 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -3830,10 +3573,6 @@ packages: resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} engines: {node: '>= 0.4'} - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - es-to-primitive@1.3.0: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} @@ -3856,70 +3595,11 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-import-resolver-node@0.3.9: - resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - - eslint-import-resolver-typescript@3.10.1: - resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - - eslint-module-utils@2.12.1: - resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - - eslint-plugin-compat@6.0.2: - resolution: {integrity: sha512-1ME+YfJjmOz1blH0nPZpHgjMGK4kjgEeoYqGCqoBPQ/mGu/dJzdoP0f1C8H2jcWZjzhZjAMccbM/VdXhPORIfA==} - engines: {node: '>=18.x'} - peerDependencies: - eslint: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-plugin-formatjs@6.2.0: resolution: {integrity: sha512-JftP9glJrS4qdviqTyZ0Kk14hcHB8AJn2FP2W7dsMugOIHDgra30mTvGjRMohivDIaFXnPGCAOv/AYm55BMUBQ==} peerDependencies: eslint: '9' - eslint-plugin-import@2.32.0: - resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - - eslint-plugin-promise@6.6.0: - resolution: {integrity: sha512-57Zzfw8G6+Gq7axm2Pdo3gW/Rx3h9Yywgn61uE/3elTCOePEHVrn2i5CdfBwA1BLK0Q0WqctICIUSqXZW/VprQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -3975,10 +3655,6 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - exifr@7.1.3: resolution: {integrity: sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==} @@ -4018,14 +3694,6 @@ packages: fasttext.wasm.js@1.0.0: resolution: {integrity: sha512-Rv2DyM9ZaJ/r09FRIYeVXxsSRFu45CVH3Zu7nheTe/EPCH0Sew1wKf0zQ4VCBmibGRBpeHlpMuRdsp+VC/YwZw==} - fdir@6.4.6: - resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -4130,6 +3798,10 @@ packages: resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==} engines: {node: '>=18'} + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4141,17 +3813,10 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - get-symbol-description@1.1.0: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -4180,10 +3845,6 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} - globals@15.15.0: - resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} - engines: {node: '>=18'} - globals@17.3.0: resolution: {integrity: sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==} engines: {node: '>=18'} @@ -4272,7 +3933,7 @@ packages: html-react-parser@5.2.17: resolution: {integrity: sha512-m+K/7Moq1jodAB4VL0RXSOmtwLUYoAsikZhwd+hGQe5Vtw2dbWfpFd60poxojMU0Tsh9w59mN1QLEcoHz0Dx9w==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 react: 0.14 || 15 || 16 || 17 || 18 || 19 peerDependenciesMeta: '@types/react': @@ -4297,10 +3958,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} @@ -4395,9 +4052,6 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} - is-bun-module@2.0.0: - resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} - is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -4426,10 +4080,6 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-fullwidth-code-point@4.0.0: - resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==} - engines: {node: '>=12'} - is-fullwidth-code-point@5.0.0: resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==} engines: {node: '>=18'} @@ -4496,10 +4146,6 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - is-string@1.1.1: resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} engines: {node: '>= 0.4'} @@ -4611,10 +4257,6 @@ packages: resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} engines: {node: '>= 0.4'} - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -4760,14 +4402,14 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - lint-staged@15.5.2: - resolution: {integrity: sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w==} - engines: {node: '>=18.12.0'} + lint-staged@16.2.7: + resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==} + engines: {node: '>=20.17'} hasBin: true - listr2@8.3.3: - resolution: {integrity: sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==} - engines: {node: '>=18.0.0'} + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} @@ -4912,10 +4554,6 @@ packages: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} @@ -4945,9 +4583,6 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} @@ -4968,16 +4603,15 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nano-spawn@2.0.0: + resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} + engines: {node: '>=20.17'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-postinstall@0.3.2: - resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - hasBin: true - natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -5000,10 +4634,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} @@ -5034,25 +4664,9 @@ packages: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.groupby@1.0.3: - resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - onetime@7.0.0: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} @@ -5537,7 +5151,7 @@ packages: react-intl@8.1.3: resolution: {integrity: sha512-eL1/d+uQdnapirynOGAriW0K9uAoyarjRGL3V9LaTRuohNSvPgCfJX06EZl5M52h/Hu7Gz7A1sD7dNHcos1lNg==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 react: '19' typescript: ^5.6.0 peerDependenciesMeta: @@ -5553,7 +5167,7 @@ packages: react-redux@9.2.0: resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 react: ^18.0 || ^19 redux: ^5.0.0 peerDependenciesMeta: @@ -5597,10 +5211,6 @@ packages: react: '>=16 || >=17 || >= 18 || >= 19' react-dom: '>=16 || >=17 || >= 18 || >=19' - react@18.3.1: - resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} - engines: {node: '>=0.10.0'} - react@19.2.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -5680,9 +5290,6 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -5999,10 +5606,6 @@ packages: resolution: {integrity: sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==} engines: {node: '>=10'} - slice-ansi@5.0.0: - resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==} - engines: {node: '>=12'} - slice-ansi@7.1.0: resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} engines: {node: '>=18'} @@ -6038,9 +5641,6 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - stable-hash@0.0.5: - resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} - stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -6070,6 +5670,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string-width@8.2.0: + resolution: {integrity: sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==} + engines: {node: '>=20'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -6101,18 +5705,14 @@ packages: resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} engines: {node: '>=12'} - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} strip-comments@2.0.1: resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==} engines: {node: '>=10'} - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -6291,10 +5891,6 @@ packages: tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyglobby@0.2.14: - resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} - engines: {node: '>=12.0.0'} - tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -6325,18 +5921,9 @@ packages: resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} engines: {node: '>=20'} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: '>=4.2.0' - ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -6395,11 +5982,6 @@ packages: engines: {node: '>=14.17'} hasBin: true - typescript@5.9.2: - resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} - engines: {node: '>=14.17'} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -6456,9 +6038,6 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} - unrs-resolver@1.11.1: - resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - upath@1.2.0: resolution: {integrity: sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==} engines: {node: '>=4'} @@ -6475,7 +6054,7 @@ packages: use-mutative@1.3.1: resolution: {integrity: sha512-5qTAr3sVVzLDU7H66oRYbSyIoWAmDT3NTr+WTJVvW+KT9Diqz31r/BLGqvVErv54TKl4wYoOOqF6qZLwGA34SA==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 mutative: ^1.3.0 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 @@ -6626,49 +6205,6 @@ packages: yaml: optional: true - vite@8.0.0-beta.14: - resolution: {integrity: sha512-oLW66oi8tZcoxu6+1HFXb+5hLHco3OnEVu2Awmj5NqEo7vxaqybjBM0BXHcq+jAFhzkMGXJl8xcO5qDBczgKLg==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.0.0-alpha.31 - esbuild: ^0.27.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -6870,7 +6406,7 @@ packages: zustand-mutative@1.3.1: resolution: {integrity: sha512-UU6d/KFdn0f3vlr0EtQGA18yZVaz8E00e0WuuxCwkOCVdmwFwSKOCAdtWgmDdwtbDKjniHCJcxaIIjyymwSk/g==} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 mutative: ^1.3.0 react: ^18.0 || ^17.0 || ^19.0 zustand: ^4.0 || ^5.0 @@ -6879,7 +6415,7 @@ packages: resolution: {integrity: sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': ^18.3.18 + '@types/react': ^19.2.14 immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -7724,33 +7260,17 @@ snapshots: '@dual-bundle/import-meta-resolve@4.1.0': {} - '@emnapi/core@1.4.5': - dependencies: - '@emnapi/wasi-threads': 1.0.4 - tslib: 2.8.1 - optional: true - '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.5': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.0.4': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/wasi-threads@1.1.0': dependencies: tslib: 2.8.1 @@ -7921,7 +7441,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.1 + debug: 4.4.3 espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -8027,7 +7547,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.1 + debug: 4.4.3 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -8259,16 +7779,6 @@ snapshots: '@material/material-color-utilities@0.3.0': {} - '@mdn/browser-compat-data@5.7.6': {} - - '@microsoft/api-extractor-model@7.30.7(@types/node@20.19.9)': - dependencies: - '@microsoft/tsdoc': 0.15.1 - '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@20.19.9) - transitivePeerDependencies: - - '@types/node' - '@microsoft/api-extractor-model@7.30.7(@types/node@25.3.0)': dependencies: '@microsoft/tsdoc': 0.15.1 @@ -8277,24 +7787,6 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@microsoft/api-extractor@7.52.10(@types/node@20.19.9)': - dependencies: - '@microsoft/api-extractor-model': 7.30.7(@types/node@20.19.9) - '@microsoft/tsdoc': 0.15.1 - '@microsoft/tsdoc-config': 0.17.1 - '@rushstack/node-core-library': 5.14.0(@types/node@20.19.9) - '@rushstack/rig-package': 0.5.3 - '@rushstack/terminal': 0.15.4(@types/node@20.19.9) - '@rushstack/ts-command-line': 5.0.2(@types/node@20.19.9) - lodash: 4.17.23 - minimatch: 10.0.3 - resolve: 1.22.10 - semver: 7.5.4 - source-map: 0.6.1 - typescript: 5.8.2 - transitivePeerDependencies: - - '@types/node' - '@microsoft/api-extractor@7.52.10(@types/node@25.3.0)': dependencies: '@microsoft/api-extractor-model': 7.30.7(@types/node@25.3.0) @@ -8324,13 +7816,6 @@ snapshots: '@mkljczk/url-purify@0.0.5': {} - '@napi-rs/wasm-runtime@0.2.12': - dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 - '@tybys/wasm-util': 0.10.0 - optional: true - '@napi-rs/wasm-runtime@1.1.1': dependencies: '@emnapi/core': 1.8.1 @@ -8350,11 +7835,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nolyfill/is-core-module@1.0.39': {} - - '@oxc-project/runtime@0.113.0': {} - - '@oxc-project/types@0.113.0': {} + '@oxc-project/types@0.113.0': + optional: true '@oxfmt/binding-android-arm-eabi@0.35.0': optional: true @@ -8654,7 +8136,7 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': dependencies: '@standard-schema/spec': 1.0.0 '@standard-schema/utils': 0.3.0 @@ -8664,7 +8146,7 @@ snapshots: reselect: 5.1.1 optionalDependencies: react: 19.2.4 - react-redux: 9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1) + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) '@rolldown/binding-android-arm64@1.0.0-rc.4': optional: true @@ -8709,7 +8191,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.3': {} - '@rolldown/pluginutils@1.0.0-rc.4': {} + '@rolldown/pluginutils@1.0.0-rc.4': + optional: true '@rollup/plugin-babel@5.3.1(@babel/core@7.29.0)(@types/babel__core@7.20.5)(rollup@2.79.2)': dependencies: @@ -8849,21 +8332,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true - '@rtsao/scc@1.1.0': {} - - '@rushstack/node-core-library@5.14.0(@types/node@20.19.9)': - dependencies: - ajv: 8.13.0 - ajv-draft-04: 1.0.0(ajv@8.13.0) - ajv-formats: 3.0.1(ajv@8.13.0) - fs-extra: 11.3.1 - import-lazy: 4.0.0 - jju: 1.4.0 - resolve: 1.22.10 - semver: 7.5.4 - optionalDependencies: - '@types/node': 20.19.9 - '@rushstack/node-core-library@5.14.0(@types/node@25.3.0)': dependencies: ajv: 8.13.0 @@ -8882,13 +8350,6 @@ snapshots: resolve: 1.22.10 strip-json-comments: 3.1.1 - '@rushstack/terminal@0.15.4(@types/node@20.19.9)': - dependencies: - '@rushstack/node-core-library': 5.14.0(@types/node@20.19.9) - supports-color: 8.1.1 - optionalDependencies: - '@types/node': 20.19.9 - '@rushstack/terminal@0.15.4(@types/node@25.3.0)': dependencies: '@rushstack/node-core-library': 5.14.0(@types/node@25.3.0) @@ -8896,15 +8357,6 @@ snapshots: optionalDependencies: '@types/node': 25.3.0 - '@rushstack/ts-command-line@5.0.2(@types/node@20.19.9)': - dependencies: - '@rushstack/terminal': 0.15.4(@types/node@20.19.9) - '@types/argparse': 1.0.38 - argparse: 1.0.10 - string-argv: 0.3.2 - transitivePeerDependencies: - - '@types/node' - '@rushstack/ts-command-line@5.0.2(@types/node@25.3.0)': dependencies: '@rushstack/terminal': 0.15.4(@types/node@25.3.0) @@ -9010,8 +8462,6 @@ snapshots: '@tanstack/devtools-event-client': 0.4.0 '@tanstack/store': 0.8.1 - '@tanstack/query-core@5.83.1': {} - '@tanstack/query-core@5.90.20': {} '@tanstack/react-pacer@0.20.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': @@ -9021,11 +8471,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - '@tanstack/react-query@5.84.1(react@18.3.1)': - dependencies: - '@tanstack/query-core': 5.83.1 - react: 18.3.1 - '@tanstack/react-query@5.90.21(react@19.2.4)': dependencies: '@tanstack/query-core': 5.90.20 @@ -9076,11 +8521,6 @@ snapshots: '@twemoji/svg@15.0.0': {} - '@tybys/wasm-util@0.10.0': - dependencies: - tslib: 2.8.1 - optional: true - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -9133,9 +8573,9 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.27)': + '@types/hoist-non-react-statics@3.3.7(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 hoist-non-react-statics: 3.3.2 '@types/http-link-header@1.0.7': @@ -9144,20 +8584,12 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/json5@0.0.29': {} - '@types/leaflet@1.9.21': dependencies: '@types/geojson': 7946.0.16 - '@types/lodash@4.17.20': {} - '@types/lodash@4.17.24': {} - '@types/node@20.19.9': - dependencies: - undici-types: 6.21.0 - '@types/node@22.19.11': dependencies: undici-types: 6.21.0 @@ -9170,38 +8602,30 @@ snapshots: '@types/picomatch@4.0.2': {} - '@types/prop-types@15.7.15': {} - - '@types/react-color@3.0.13(@types/react@18.3.27)': + '@types/react-color@3.0.13(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.27 - '@types/reactcss': 1.2.13(@types/react@18.3.27) + '@types/react': 19.2.14 + '@types/reactcss': 1.2.13(@types/react@19.2.14) - '@types/react-dom@18.3.7(@types/react@18.3.27)': + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 '@types/react-sparklines@1.7.5': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 '@types/react-swipeable-views@0.13.6': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 - '@types/react@18.3.23': + '@types/react@19.2.14': dependencies: - '@types/prop-types': 15.7.15 - csstype: 3.1.3 - - '@types/react@18.3.27': - dependencies: - '@types/prop-types': 15.7.15 csstype: 3.2.3 - '@types/reactcss@1.2.13(@types/react@18.3.27)': + '@types/reactcss@1.2.13(@types/react@19.2.14)': dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 '@types/resolve@1.20.2': {} @@ -9213,87 +8637,6 @@ snapshots: '@types/use-sync-external-store@0.0.6': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 8.57.1 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1 - eslint: 8.57.1 - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - - '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) - '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - debug: 4.4.1 - eslint: 8.57.1 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@7.18.0': {} - - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.2)': - dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.4.1 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.4 - ts-api-utils: 1.4.3(typescript@5.9.2) - optionalDependencies: - typescript: 5.9.2 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': - dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) - eslint: 8.57.1 - transitivePeerDependencies: - - supports-color - - typescript - - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - '@uidotdev/usehooks@2.4.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: react: 19.2.4 @@ -9303,65 +8646,6 @@ snapshots: '@unicode/unicode-17.0.0@1.6.16': {} - '@unrs/resolver-binding-android-arm-eabi@1.11.1': - optional: true - - '@unrs/resolver-binding-android-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-arm64@1.11.1': - optional: true - - '@unrs/resolver-binding-darwin-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-freebsd-x64@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-arm64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-gnu@1.11.1': - optional: true - - '@unrs/resolver-binding-linux-x64-musl@1.11.1': - optional: true - - '@unrs/resolver-binding-wasm32-wasi@1.11.1': - dependencies: - '@napi-rs/wasm-runtime': 0.2.12 - optional: true - - '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': - optional: true - - '@unrs/resolver-binding-win32-x64-msvc@1.11.1': - optional: true - '@use-gesture/core@10.3.1': {} '@use-gesture/react@10.3.1(react@19.2.4)': @@ -9428,19 +8712,6 @@ snapshots: de-indent: 1.0.2 he: 1.2.0 - '@vue/language-core@2.2.0(typescript@5.9.2)': - dependencies: - '@volar/language-core': 2.4.22 - '@vue/compiler-dom': 3.5.18 - '@vue/compiler-vue2': 2.7.16 - '@vue/shared': 3.5.18 - alien-signals: 0.4.14 - minimatch: 9.0.5 - muggle-string: 0.4.1 - path-browserify: 1.0.1 - optionalDependencies: - typescript: 5.9.2 - '@vue/language-core@2.2.0(typescript@5.9.3)': dependencies: '@volar/language-core': 2.4.22 @@ -9644,43 +8915,8 @@ snapshots: call-bound: 1.0.4 is-array-buffer: 3.0.5 - array-includes@3.1.9: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - array-union@2.1.0: {} - array.prototype.findlastindex@1.2.6: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-shim-unscopables: 1.1.0 - arraybuffer.prototype.slice@1.0.4: dependencies: array-buffer-byte-length: 1.0.2 @@ -9691,10 +8927,6 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - ast-metadata-inferer@0.8.1: - dependencies: - '@mdn/browser-compat-data': 5.7.6 - astral-regex@2.0.0: {} async-function@1.0.0: {} @@ -9838,8 +9070,6 @@ snapshots: lodash.memoize: 4.1.2 lodash.uniq: 4.5.0 - caniuse-lite@1.0.30001731: {} - caniuse-lite@1.0.30001762: {} caniuse-lite@1.0.30001774: {} @@ -9849,8 +9079,6 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chalk@5.5.0: {} - char-regex@1.0.2: {} chokidar@3.6.0: @@ -9879,10 +9107,10 @@ snapshots: dependencies: restore-cursor: 5.1.0 - cli-truncate@4.0.0: + cli-truncate@5.1.1: dependencies: - slice-ansi: 5.0.0 - string-width: 7.2.0 + slice-ansi: 7.1.0 + string-width: 8.2.0 clsx@2.1.1: {} @@ -9898,7 +9126,7 @@ snapshots: colorjs.io@0.5.2: {} - commander@13.1.0: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -10082,10 +9310,6 @@ snapshots: de-indent@1.0.2: {} - debug@3.2.7: - dependencies: - ms: 2.1.3 - debug@4.4.1: dependencies: ms: 2.1.3 @@ -10119,7 +9343,8 @@ snapshots: detect-libc@1.0.3: optional: true - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true detect-passive-events@2.0.3: dependencies: @@ -10135,10 +9360,6 @@ snapshots: dlv@1.1.3: {} - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -10309,10 +9530,6 @@ snapshots: has-tostringtag: 1.0.2 hasown: 2.0.2 - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.2 - es-to-primitive@1.3.0: dependencies: is-callable: 1.2.7 @@ -10380,52 +9597,6 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-import-resolver-node@0.3.9: - dependencies: - debug: 3.2.7 - is-core-module: 2.16.1 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.4.1 - eslint: 8.57.1 - get-tsconfig: 4.10.1 - is-bun-module: 2.0.0 - stable-hash: 0.0.5 - tinyglobby: 0.2.14 - unrs-resolver: 1.11.1 - optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) - transitivePeerDependencies: - - supports-color - - eslint-plugin-compat@6.0.2(eslint@8.57.1): - dependencies: - '@mdn/browser-compat-data': 5.7.6 - ast-metadata-inferer: 0.8.1 - browserslist: 4.28.1 - caniuse-lite: 1.0.30001731 - eslint: 8.57.1 - find-up: 5.0.0 - globals: 15.15.0 - lodash.memoize: 4.1.2 - semver: 7.7.4 - eslint-plugin-formatjs@6.2.0(eslint@8.57.1): dependencies: '@formatjs/icu-messageformat-parser': 3.5.1 @@ -10439,39 +9610,6 @@ snapshots: transitivePeerDependencies: - ts-jest - eslint-plugin-import@2.32.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.9 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 - optionalDependencies: - '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - - eslint-plugin-promise@6.6.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -10497,7 +9635,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.1 + debug: 4.4.3 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -10555,18 +9693,6 @@ snapshots: events@3.3.0: {} - execa@8.0.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - exifr@7.1.3: {} exsolve@1.0.7: {} @@ -10601,10 +9727,6 @@ snapshots: dependencies: '@types/emscripten': 1.40.1 - fdir@6.4.6(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -10713,6 +9835,8 @@ snapshots: get-east-asian-width@1.3.0: {} + get-east-asian-width@1.5.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -10733,18 +9857,12 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@8.0.1: {} - get-symbol-description@1.1.0: dependencies: call-bound: 1.0.4 es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.10.1: - dependencies: - resolve-pkg-maps: 1.0.0 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -10783,8 +9901,6 @@ snapshots: dependencies: type-fest: 0.20.2 - globals@15.15.0: {} - globals@17.3.0: {} globalthis@1.0.4: @@ -10871,7 +9987,7 @@ snapshots: relateurl: 0.2.7 terser: 5.43.1 - html-react-parser@5.2.17(@types/react@18.3.27)(react@19.2.4): + html-react-parser@5.2.17(@types/react@19.2.14)(react@19.2.4): dependencies: domhandler: 5.0.3 html-dom-parser: 5.1.8 @@ -10879,7 +9995,7 @@ snapshots: react-property: 2.0.2 style-to-js: 1.1.21 optionalDependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 html-tags@3.3.1: {} @@ -10906,8 +10022,6 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@5.0.0: {} - husky@9.1.7: {} idb@7.1.1: {} @@ -10997,10 +10111,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-bun-module@2.0.0: - dependencies: - semver: 7.7.4 - is-callable@1.2.7: {} is-core-module@2.16.1: @@ -11026,8 +10136,6 @@ snapshots: is-fullwidth-code-point@3.0.0: {} - is-fullwidth-code-point@4.0.0: {} - is-fullwidth-code-point@5.0.0: dependencies: get-east-asian-width: 1.3.0 @@ -11081,8 +10189,6 @@ snapshots: is-stream@2.0.1: {} - is-stream@3.0.0: {} - is-string@1.1.1: dependencies: call-bound: 1.0.4 @@ -11144,7 +10250,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 20.19.9 + '@types/node': 25.3.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11209,10 +10315,6 @@ snapshots: jsonify: 0.0.1 object-keys: 1.1.1 - json5@1.0.2: - dependencies: - minimist: 1.2.8 - json5@2.2.3: {} jsonfile@6.1.0: @@ -11316,6 +10418,7 @@ snapshots: lightningcss-linux-x64-musl: 1.31.1 lightningcss-win32-arm64-msvc: 1.31.1 lightningcss-win32-x64-msvc: 1.31.1 + optional: true lilconfig@3.1.3: {} @@ -11327,24 +10430,19 @@ snapshots: dependencies: uc.micro: 2.1.0 - lint-staged@15.5.2: + lint-staged@16.2.7: dependencies: - chalk: 5.5.0 - commander: 13.1.0 - debug: 4.4.1 - execa: 8.0.1 - lilconfig: 3.1.3 - listr2: 8.3.3 + commander: 14.0.3 + listr2: 9.0.5 micromatch: 4.0.8 + nano-spawn: 2.0.0 pidtree: 0.6.0 string-argv: 0.3.2 - yaml: 2.8.0 - transitivePeerDependencies: - - supports-color + yaml: 2.8.2 - listr2@8.3.3: + listr2@9.0.5: dependencies: - cli-truncate: 4.0.0 + cli-truncate: 5.1.1 colorette: 2.0.20 eventemitter3: 5.0.1 log-update: 6.1.0 @@ -11477,8 +10575,6 @@ snapshots: dependencies: mime-db: 1.52.0 - mimic-fn@4.0.0: {} - mimic-function@5.0.1: {} mini-css-extract-plugin@2.10.0(webpack@5.101.0(esbuild@0.24.2)): @@ -11505,8 +10601,6 @@ snapshots: dependencies: brace-expansion: 2.0.2 - minimist@1.2.8: {} - minipass@7.1.2: {} mlly@1.7.4: @@ -11528,9 +10622,9 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 - nanoid@3.3.11: {} + nano-spawn@2.0.0: {} - napi-postinstall@0.3.2: {} + nanoid@3.3.11: {} natural-compare@1.4.0: {} @@ -11553,10 +10647,6 @@ snapshots: normalize-path@3.0.0: {} - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - npm-run-path@6.0.0: dependencies: path-key: 4.0.0 @@ -11585,34 +10675,10 @@ snapshots: has-symbols: 1.1.0 object-keys: 1.1.1 - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - es-object-atoms: 1.1.1 - - object.groupby@1.0.3: - dependencies: - call-bind: 1.0.8 - define-properties: 1.2.1 - es-abstract: 1.24.0 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.8 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - once@1.4.0: dependencies: wrappy: 1.0.2 - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - onetime@7.0.0: dependencies: mimic-function: 5.0.1 @@ -12095,13 +11161,13 @@ snapshots: react: 19.2.4 react-from-dom: 0.7.5(react@19.2.4) - react-intl@8.1.3(@types/react@18.3.27)(react@19.2.4)(typescript@5.9.3): + react-intl@8.1.3(@types/react@19.2.14)(react@19.2.4)(typescript@5.9.3): dependencies: '@formatjs/ecma402-abstract': 3.1.1 '@formatjs/icu-messageformat-parser': 3.5.1 '@formatjs/intl': 4.1.2(typescript@5.9.3) - '@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.27) - '@types/react': 18.3.27 + '@types/hoist-non-react-statics': 3.3.7(@types/react@19.2.14) + '@types/react': 19.2.14 hoist-non-react-statics: 3.3.2 intl-messageformat: 11.1.2 react: 19.2.4 @@ -12113,13 +11179,13 @@ snapshots: react-property@2.0.2: {} - react-redux@9.2.0(@types/react@18.3.27)(react@19.2.4)(redux@5.0.1): + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): dependencies: '@types/use-sync-external-store': 0.0.6 react: 19.2.4 use-sync-external-store: 1.5.0(react@19.2.4) optionalDependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 redux: 5.0.1 react-refresh@0.18.0: {} @@ -12164,10 +11230,6 @@ snapshots: react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - react@18.3.1: - dependencies: - loose-envify: 1.4.0 - react@19.2.4: {} reactcss@1.2.3(react@19.2.4): @@ -12246,8 +11308,6 @@ snapshots: resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -12291,6 +11351,7 @@ snapshots: '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4 '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4 + optional: true rollup-plugin-bundle-stats@4.21.10(core-js@3.48.0)(rolldown@1.0.0-rc.4)(rollup@2.79.2)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: @@ -12581,11 +11642,6 @@ snapshots: astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 - slice-ansi@5.0.0: - dependencies: - ansi-styles: 6.2.1 - is-fullwidth-code-point: 4.0.0 - slice-ansi@7.1.0: dependencies: ansi-styles: 6.2.1 @@ -12612,8 +11668,6 @@ snapshots: sprintf-js@1.0.3: {} - stable-hash@0.0.5: {} - stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -12647,6 +11701,11 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.0 + string-width@8.2.0: + dependencies: + get-east-asian-width: 1.5.0 + strip-ansi: 7.1.2 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -12704,12 +11763,12 @@ snapshots: dependencies: ansi-regex: 6.1.0 - strip-bom@3.0.0: {} + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.1.0 strip-comments@2.0.1: {} - strip-final-newline@3.0.0: {} - strip-json-comments@3.1.1: {} style-to-js@1.1.21: @@ -12948,11 +12007,6 @@ snapshots: tinycolor2@1.6.0: {} - tinyglobby@0.2.14: - dependencies: - fdir: 6.4.6(picomatch@4.0.3) - picomatch: 4.0.3 - tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -12982,19 +12036,8 @@ snapshots: dependencies: punycode: 2.3.1 - ts-api-utils@1.4.3(typescript@5.9.2): - dependencies: - typescript: 5.9.2 - ts-interface-checker@0.1.13: {} - tsconfig-paths@3.15.0: - dependencies: - '@types/json5': 0.0.29 - json5: 1.0.2 - minimist: 1.2.8 - strip-bom: 3.0.0 - tslib@2.8.1: {} type-check@0.4.0: @@ -13060,8 +12103,6 @@ snapshots: typescript@5.8.2: {} - typescript@5.9.2: {} - typescript@5.9.3: {} uc.micro@2.1.0: {} @@ -13105,30 +12146,6 @@ snapshots: universalify@2.0.1: {} - unrs-resolver@1.11.1: - dependencies: - napi-postinstall: 0.3.2 - optionalDependencies: - '@unrs/resolver-binding-android-arm-eabi': 1.11.1 - '@unrs/resolver-binding-android-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-arm64': 1.11.1 - '@unrs/resolver-binding-darwin-x64': 1.11.1 - '@unrs/resolver-binding-freebsd-x64': 1.11.1 - '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 - '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 - '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 - '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 - '@unrs/resolver-binding-linux-x64-musl': 1.11.1 - '@unrs/resolver-binding-wasm32-wasi': 1.11.1 - '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 - '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 - '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - upath@1.2.0: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -13141,9 +12158,9 @@ snapshots: dependencies: punycode: 2.3.1 - use-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4): + use-mutative@1.3.1(@types/react@19.2.14)(mutative@1.3.0)(react@19.2.4): dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 mutative: 1.3.0 react: 19.2.4 @@ -13165,10 +12182,6 @@ snapshots: is-typed-array: 1.1.15 which-typed-array: 1.1.19 - valibot@1.2.0(typescript@5.9.2): - optionalDependencies: - typescript: 5.9.2 - valibot@1.2.0(typescript@5.9.3): optionalDependencies: typescript: 5.9.3 @@ -13209,25 +12222,6 @@ snapshots: transitivePeerDependencies: - supports-color - vite-plugin-dts@4.5.4(@types/node@20.19.9)(rollup@4.57.1)(typescript@5.9.2)(vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): - dependencies: - '@microsoft/api-extractor': 7.52.10(@types/node@20.19.9) - '@rollup/pluginutils': 5.2.0(rollup@4.57.1) - '@volar/typescript': 2.4.22 - '@vue/language-core': 2.2.0(typescript@5.9.2) - compare-versions: 6.1.1 - debug: 4.4.1 - kolorist: 1.8.0 - local-pkg: 1.1.1 - magic-string: 0.30.17 - typescript: 5.9.2 - optionalDependencies: - vite: 8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite-plugin-dts@4.5.4(@types/node@25.3.0)(rollup@4.57.1)(typescript@5.9.3)(vite@7.3.1(@types/node@25.3.0)(jiti@1.21.7)(lightningcss@1.31.1)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2)): dependencies: '@microsoft/api-extractor': 7.52.10(@types/node@25.3.0) @@ -13318,25 +12312,6 @@ snapshots: terser: 5.46.0 yaml: 2.8.2 - vite@8.0.0-beta.14(@types/node@20.19.9)(esbuild@0.27.3)(jiti@1.21.7)(sass-embedded@1.97.3)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.2): - dependencies: - '@oxc-project/runtime': 0.113.0 - fdir: 6.5.0(picomatch@4.0.3) - lightningcss: 1.31.1 - picomatch: 4.0.3 - postcss: 8.5.6 - rolldown: 1.0.0-rc.4 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 20.19.9 - esbuild: 0.27.3 - fsevents: 2.3.3 - jiti: 1.21.7 - sass: 1.97.3 - sass-embedded: 1.97.3 - terser: 5.46.0 - yaml: 2.8.2 - vscode-uri@3.1.0: {} vue-loader@17.4.2(@vue/compiler-sfc@3.5.18)(webpack@5.101.0(esbuild@0.24.2)): @@ -13624,16 +12599,16 @@ snapshots: yocto-queue@0.1.0: {} - zustand-mutative@1.3.1(@types/react@18.3.27)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))): + zustand-mutative@1.3.1(@types/react@19.2.14)(mutative@1.3.0)(react@19.2.4)(zustand@5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))): dependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 mutative: 1.3.0 react: 19.2.4 - zustand: 5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + zustand: 5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) - zustand@5.0.11(@types/react@18.3.27)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + zustand@5.0.11(@types/react@19.2.14)(immer@10.1.1)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: - '@types/react': 18.3.27 + '@types/react': 19.2.14 immer: 10.1.1 react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) From 77678d5452e5cae17972a264386b564ddc89351c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 16:30:03 +0100 Subject: [PATCH 053/264] nicolium: react 19 types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/columns/notifications.tsx | 6 +- packages/pl-fe/src/columns/search.tsx | 2 +- packages/pl-fe/src/columns/trends.tsx | 2 +- .../src/components/account-hover-card.tsx | 2 +- .../pl-fe/src/components/alt-indicator.tsx | 2 +- .../components/authorize-reject-buttons.tsx | 4 +- .../dropdown-menu/dropdown-menu.tsx | 4 +- .../src/components/dropdown-navigation.tsx | 4 +- .../src/components/hover-status-wrapper.tsx | 2 +- packages/pl-fe/src/components/list.tsx | 59 ++++++++++--------- .../src/components/missing-indicator.tsx | 2 +- packages/pl-fe/src/components/modal-root.tsx | 2 +- .../pl-fe/src/components/parsed-content.tsx | 2 +- packages/pl-fe/src/components/parsed-mfm.tsx | 6 +- .../src/components/polls/poll-footer.tsx | 2 +- .../src/components/polls/poll-option.tsx | 4 +- packages/pl-fe/src/components/polls/poll.tsx | 2 +- .../pl-fe/src/components/preview-card.tsx | 2 +- .../pl-fe/src/components/pull-to-refresh.tsx | 6 +- .../pl-fe/src/components/scrollable-list.tsx | 8 +-- .../components/sidebar-navigation-link.tsx | 5 +- .../src/components/status-action-button.tsx | 4 +- packages/pl-fe/src/components/status-list.tsx | 2 +- .../pl-fe/src/components/status-media.tsx | 8 +-- .../pl-fe/src/components/status-mention.tsx | 2 +- .../src/components/thumb-navigation-link.tsx | 2 +- .../pl-fe/src/components/thumb-navigation.tsx | 2 +- .../pl-fe/src/components/ui/button/index.tsx | 2 +- packages/pl-fe/src/components/ui/card.tsx | 8 +-- packages/pl-fe/src/components/ui/column.tsx | 2 +- packages/pl-fe/src/components/ui/emoji.tsx | 2 +- packages/pl-fe/src/components/ui/hstack.tsx | 2 +- .../pl-fe/src/components/ui/icon-button.tsx | 2 +- packages/pl-fe/src/components/ui/icon.tsx | 2 +- packages/pl-fe/src/components/ui/popover.tsx | 2 +- packages/pl-fe/src/components/ui/stack.tsx | 2 +- .../pl-fe/src/components/ui/streamfield.tsx | 4 +- packages/pl-fe/src/components/ui/svg-icon.tsx | 2 +- packages/pl-fe/src/components/ui/tabs.tsx | 4 +- packages/pl-fe/src/components/ui/tooltip.tsx | 2 +- packages/pl-fe/src/components/ui/widget.tsx | 4 +- .../src/features/admin/components/counter.tsx | 2 +- .../features/admin/components/dimension.tsx | 2 +- .../chats/components/chat-message-list.tsx | 2 +- .../components/chat-search/chat-search.tsx | 1 - .../chats/components/chat-search/results.tsx | 3 +- .../components/chat-widget/chat-window.tsx | 2 +- .../components/shoutbox-message-list.tsx | 2 +- .../compose/components/compose-form.tsx | 4 +- .../compose/components/upload-form.tsx | 4 +- .../compose/editor/nodes/emoji-node.tsx | 4 +- .../compose/editor/nodes/image-component.tsx | 4 +- .../compose/editor/nodes/image-node.tsx | 4 +- .../compose/editor/nodes/mention-node.tsx | 4 +- .../editor/plugins/autosuggest-plugin.tsx | 8 +-- .../floating-block-type-toolbar-plugin.tsx | 6 +- .../plugins/floating-link-editor-plugin.tsx | 6 +- .../floating-text-format-toolbar-plugin.tsx | 6 +- .../compose/editor/plugins/link-plugin.tsx | 2 +- .../components/conversations-list.tsx | 2 +- .../components/crypto-address.tsx | 2 +- .../components/crypto-donate-panel.tsx | 4 +- .../crypto-donate/components/crypto-icon.tsx | 2 +- .../components/detailed-crypto-address.tsx | 2 +- .../components/lightning-address.tsx | 2 +- .../crypto-donate/components/site-wallet.tsx | 2 +- .../emoji-picker-dropdown-container.tsx | 2 +- .../notifications/components/notification.tsx | 2 +- .../components/status-interaction-bar.tsx | 4 +- .../status/components/thread-status.tsx | 4 +- .../src/features/status/components/thread.tsx | 2 +- .../src/features/ui/components/hotkeys.tsx | 2 +- .../features/ui/components/link-footer.tsx | 2 +- .../ui/components/panels/birthday-panel.tsx | 2 +- .../ui/components/panels/user-panel.tsx | 4 +- .../ui/components/profile-dropdown.tsx | 2 +- .../features/ui/components/zoomable-image.tsx | 2 +- .../pl-fe/src/features/ui/router/index.tsx | 2 +- .../pl-fe/src/hooks/forms/use-image-field.ts | 2 +- packages/pl-fe/src/hooks/use-dragged-files.ts | 2 +- packages/pl-fe/src/hooks/use-long-press.ts | 2 +- packages/pl-fe/src/hooks/use-previous.ts | 4 +- .../pl-fe/src/modals/dropdown-menu-modal.tsx | 2 +- packages/pl-fe/src/modals/event-map-modal.tsx | 2 +- packages/pl-fe/src/modals/hotkeys-modal.tsx | 4 +- .../status-lists/interaction-requests.tsx | 2 +- .../src/pages/statuses/event-discussion.tsx | 4 +- .../pl-fe/src/pages/utils/crypto-donate.tsx | 2 +- packages/pl-fe/src/utils/scroll-utils.ts | 2 +- 89 files changed, 167 insertions(+), 161 deletions(-) diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 6dc593a7b..94cb69b1e 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -221,8 +221,8 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => }, [notifications, topNotification]); const hasMore = hasNextPage ?? false; - const node = useRef(null); - const scrollableContentRef = useRef | null>(null); + const node = useRef(null); + const scrollableContentRef = useRef | null>(null); const handleLoadOlder = useCallback( debounce( @@ -307,7 +307,7 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => /> ); - let scrollableContent: Array | null = null; + let scrollableContent: Array | null = null; const filterBarContainer = showFilterBar ? : null; diff --git a/packages/pl-fe/src/columns/search.tsx b/packages/pl-fe/src/columns/search.tsx index ca6065b5f..19a6f0fe3 100644 --- a/packages/pl-fe/src/columns/search.tsx +++ b/packages/pl-fe/src/columns/search.tsx @@ -30,7 +30,7 @@ interface ISearchColumn { const SearchColumn: React.FC = ({ type, query, accountId, multiColumn }) => { query = query.trim(); - const node = useRef(null); + const node = useRef(null); const searchAccountsQuery = useSearchAccounts((type === 'accounts' && query) || ''); const searchStatusesQuery = useSearchStatuses((type === 'statuses' && query) || '', { diff --git a/packages/pl-fe/src/columns/trends.tsx b/packages/pl-fe/src/columns/trends.tsx index 4f45fbd24..0dfba0aff 100644 --- a/packages/pl-fe/src/columns/trends.tsx +++ b/packages/pl-fe/src/columns/trends.tsx @@ -16,7 +16,7 @@ import { useTrendingStatuses } from '@/queries/trends/use-trending-statuses'; interface ITrendsColumn { type: 'accounts' | 'hashtags' | 'statuses' | 'links'; - emptyMessage?: JSX.Element; + emptyMessage?: React.JSX.Element; multiColumn?: boolean; } diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index b82215233..18ad82a32 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -32,7 +32,7 @@ const messages = { pronouns: { id: 'account.pronouns.with_label', defaultMessage: 'Pronouns: {pronouns}' }, }; -const getBadges = (account?: Pick): JSX.Element[] => { +const getBadges = (account?: Pick): React.JSX.Element[] => { const badges = []; if (account?.is_admin) { diff --git a/packages/pl-fe/src/components/alt-indicator.tsx b/packages/pl-fe/src/components/alt-indicator.tsx index 944bbe05b..6bb3425f9 100644 --- a/packages/pl-fe/src/components/alt-indicator.tsx +++ b/packages/pl-fe/src/components/alt-indicator.tsx @@ -6,7 +6,7 @@ import Icon from '@/components/ui/icon'; interface IAltIndicator extends Pick, 'title' | 'className'> { warning?: boolean; - message?: JSX.Element; + message?: React.JSX.Element; } const AltIndicator: React.FC = React.forwardRef( diff --git a/packages/pl-fe/src/components/authorize-reject-buttons.tsx b/packages/pl-fe/src/components/authorize-reject-buttons.tsx index 89051ab03..524f146f8 100644 --- a/packages/pl-fe/src/components/authorize-reject-buttons.tsx +++ b/packages/pl-fe/src/components/authorize-reject-buttons.tsx @@ -21,8 +21,8 @@ const AuthorizeRejectButtons: React.FC = ({ const [state, setState] = useState< 'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending' >('pending'); - const timeout = useRef(); - const interval = useRef>(); + const timeout = useRef(null); + const interval = useRef | null>(null); const [progress, setProgress] = useState(0); diff --git a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx index 90ad4b09b..697bcabaf 100644 --- a/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx +++ b/packages/pl-fe/src/components/dropdown-menu/dropdown-menu.tsx @@ -36,7 +36,7 @@ interface IDropdownMenuContent { } interface IDropdownMenu { - children?: React.ReactElement; + children?: React.ReactElement; disabled?: boolean; items?: Menu; component?: React.FC<{ handleClose: () => any }>; @@ -211,7 +211,7 @@ const DropdownMenuContent: React.FC = ({ ); }; -const DropdownMenu = (props: IDropdownMenu) => { +const DropdownMenu: React.FC = (props) => { const { children, disabled, diff --git a/packages/pl-fe/src/components/dropdown-navigation.tsx b/packages/pl-fe/src/components/dropdown-navigation.tsx index e73642218..0a6172a04 100644 --- a/packages/pl-fe/src/components/dropdown-navigation.tsx +++ b/packages/pl-fe/src/components/dropdown-navigation.tsx @@ -32,7 +32,7 @@ import type { Account as AccountEntity } from 'pl-api'; interface IDropdownNavigationLink extends Partial { href?: string; icon: string; - text: string | JSX.Element; + text: string | React.JSX.Element; onClick: React.EventHandler; } @@ -66,7 +66,7 @@ const DropdownNavigationLink: React.FC = React.memo( }, ); -const DropdownNavigation: React.FC = React.memo((): JSX.Element | null => { +const DropdownNavigation: React.FC = React.memo((): React.JSX.Element | null => { const dispatch = useAppDispatch(); const isSidebarOpen = useIsSidebarOpen(); diff --git a/packages/pl-fe/src/components/hover-status-wrapper.tsx b/packages/pl-fe/src/components/hover-status-wrapper.tsx index d02cdd4b9..0ca992dbb 100644 --- a/packages/pl-fe/src/components/hover-status-wrapper.tsx +++ b/packages/pl-fe/src/components/hover-status-wrapper.tsx @@ -26,7 +26,7 @@ const HoverStatusWrapper: React.FC = ({ const { openStatusHoverCard, closeStatusHoverCard } = useStatusHoverCardActions(); const ref = useRef(null); - const Elem: keyof JSX.IntrinsicElements = inline ? 'span' : 'div'; + const Elem: keyof React.JSX.IntrinsicElements = inline ? 'span' : 'div'; const handleMouseEnter = () => { if (!isMobile(window.innerWidth)) { diff --git a/packages/pl-fe/src/components/list.tsx b/packages/pl-fe/src/components/list.tsx index d11bbc74f..b31a8b3ae 100644 --- a/packages/pl-fe/src/components/list.tsx +++ b/packages/pl-fe/src/components/list.tsx @@ -20,7 +20,7 @@ type IListItem = { href?: string; onClick?(): void; isSelected?: boolean; - children?: React.ReactNode; + children?: React.ReactElement | Array>; size?: 'sm' | 'md'; } & (LinkOptions | {}); @@ -49,36 +49,37 @@ const ListItem: React.FC = ({ const renderChildren = React.useCallback( () => - React.Children.map(children, (child) => { - if (React.isValidElement(child)) { - const isSelect = child.type === SelectDropdown || child.type === Select; - const childLabelledBy = child.props['aria-labelledby']; - const childDescribedBy = child.props['aria-describedby']; - const ariaLabelledBy = childLabelledBy ? `${childLabelledBy} ${labelId}` : labelId; - const ariaDescribedBy = hint - ? childDescribedBy - ? `${childDescribedBy} ${hintId}` - : hintId - : childDescribedBy; + children + ? React.Children.map(children, (child: React.ReactElement) => { + if (React.isValidElement(child)) { + const props = child.props as any; + const isSelect = child.type === SelectDropdown || child.type === Select; + const childLabelledBy = props['aria-labelledby']; + const childDescribedBy = props['aria-describedby']; + const ariaLabelledBy = childLabelledBy ? `${childLabelledBy} ${labelId}` : labelId; + const ariaDescribedBy = hint + ? childDescribedBy + ? `${childDescribedBy} ${hintId}` + : hintId + : childDescribedBy; - return React.cloneElement(child, { - // @ts-ignore - id: domId, - // @ts-ignore - 'aria-labelledby': ariaLabelledBy, - // @ts-ignore - 'aria-describedby': ariaDescribedBy, - className: clsx( - { - 'w-auto': isSelect, - }, - child.props.className, - ), - }); - } + return React.cloneElement(child, { + // @ts-ignore + id: domId, + 'aria-labelledby': ariaLabelledBy, + 'aria-describedby': ariaDescribedBy, + className: clsx( + { + 'w-auto': isSelect, + }, + props.className, + ), + }); + } - return null; - }), + return null; + }) + : null, [children, domId, labelId, hint, hintId], ); diff --git a/packages/pl-fe/src/components/missing-indicator.tsx b/packages/pl-fe/src/components/missing-indicator.tsx index e34985f83..e3469b6d9 100644 --- a/packages/pl-fe/src/components/missing-indicator.tsx +++ b/packages/pl-fe/src/components/missing-indicator.tsx @@ -9,7 +9,7 @@ interface MissingIndicatorProps { nested?: boolean; } -const MissingIndicator = ({ nested = false }: MissingIndicatorProps): JSX.Element => ( +const MissingIndicator = ({ nested = false }: MissingIndicatorProps): React.JSX.Element => ( diff --git a/packages/pl-fe/src/components/modal-root.tsx b/packages/pl-fe/src/components/modal-root.tsx index fd32ed3bb..b65d2262b 100644 --- a/packages/pl-fe/src/components/modal-root.tsx +++ b/packages/pl-fe/src/components/modal-root.tsx @@ -51,7 +51,7 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo const activeElement = useRef( revealed ? (document.activeElement as HTMLDivElement | null) : null, ); - const unlistenHistory = useRef<() => void>(); + const unlistenHistory = useRef<(() => void) | null>(null); const prevChildren = usePrevious(children); diff --git a/packages/pl-fe/src/components/parsed-content.tsx b/packages/pl-fe/src/components/parsed-content.tsx index cb7cbaf7d..cd63ed05d 100644 --- a/packages/pl-fe/src/components/parsed-content.tsx +++ b/packages/pl-fe/src/components/parsed-content.tsx @@ -348,7 +348,7 @@ function parseContent( return transformText(reactNode, index); } - return reactNode as JSX.Element; + return reactNode as React.JSX.Element; }, }; diff --git a/packages/pl-fe/src/components/parsed-mfm.tsx b/packages/pl-fe/src/components/parsed-mfm.tsx index 739193dc5..f504e3479 100644 --- a/packages/pl-fe/src/components/parsed-mfm.tsx +++ b/packages/pl-fe/src/components/parsed-mfm.tsx @@ -3,7 +3,7 @@ import { Link } from '@tanstack/react-router'; import * as mfm from '@transfem-org/sfm-js'; import clamp from 'lodash/clamp'; -import React, { CSSProperties } from 'react'; +import React, { type CSSProperties } from 'react'; import { useSettings } from '@/stores/settings'; import { makeEmojiMap } from '@/utils/normalizers'; @@ -58,14 +58,14 @@ const ParsedMfm: React.FC = React.memo(({ text, emojis, mentions, sp const genEl = (ast: mfm.MfmNode[], scale: number) => ast - .map((token): JSX.Element | string | (JSX.Element | string)[] => { + .map((token): React.JSX.Element | string | (React.JSX.Element | string)[] => { switch (token.type) { case 'text': { let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); if (speakAsCat) text = nyaize(text); - const res: (JSX.Element | string)[] = []; + const res: (React.JSX.Element | string)[] = []; for (const t of text.split('\n')) { res.push(
); res.push(t); diff --git a/packages/pl-fe/src/components/polls/poll-footer.tsx b/packages/pl-fe/src/components/polls/poll-footer.tsx index 2d693efb3..6a0772742 100644 --- a/packages/pl-fe/src/components/polls/poll-footer.tsx +++ b/packages/pl-fe/src/components/polls/poll-footer.tsx @@ -34,7 +34,7 @@ const PollFooter: React.FC = ({ showResults, selected, statusId, -}): JSX.Element => { +}): React.JSX.Element => { const intl = useIntl(); const { refetch } = usePollQuery(poll.id); diff --git a/packages/pl-fe/src/components/polls/poll-option.tsx b/packages/pl-fe/src/components/polls/poll-option.tsx index 7f3687fc1..7856031fd 100644 --- a/packages/pl-fe/src/components/polls/poll-option.tsx +++ b/packages/pl-fe/src/components/polls/poll-option.tsx @@ -19,7 +19,7 @@ const messages = defineMessages({ const PollPercentageBar: React.FC<{ percent: number; leading: boolean }> = ({ percent, leading, -}): JSX.Element => { +}): React.JSX.Element => { const styles = useSpring({ from: { width: '0%' }, to: { width: `${percent}%` }, @@ -132,7 +132,7 @@ interface IPollOption { truncate?: boolean; } -const PollOption: React.FC = (props): JSX.Element | null => { +const PollOption: React.FC = (props): React.JSX.Element | null => { const { index, poll, option, showResults, language, truncate } = props; const intl = useIntl(); diff --git a/packages/pl-fe/src/components/polls/poll.tsx b/packages/pl-fe/src/components/polls/poll.tsx index 2e32bad1b..9cce4d0ee 100644 --- a/packages/pl-fe/src/components/polls/poll.tsx +++ b/packages/pl-fe/src/components/polls/poll.tsx @@ -22,7 +22,7 @@ interface IPoll { truncate?: boolean; } -const Poll: React.FC = ({ id, status, language, truncate }): JSX.Element | null => { +const Poll: React.FC = ({ id, status, language, truncate }): React.JSX.Element | null => { const { openModal } = useModalsActions(); const isLoggedIn = useAppSelector((state) => state.me); diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index 9203a82e7..dee55b1a4 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -105,7 +105,7 @@ const PreviewCard: React.FC = ({ compact = false, cacheWidth, onOpenMedia, -}): JSX.Element => { +}): React.JSX.Element => { const { urlPrivacy: { clearLinksInContent, redirectLinksMode }, } = useSettings(); diff --git a/packages/pl-fe/src/components/pull-to-refresh.tsx b/packages/pl-fe/src/components/pull-to-refresh.tsx index 4fcc785b5..346d7e401 100644 --- a/packages/pl-fe/src/components/pull-to-refresh.tsx +++ b/packages/pl-fe/src/components/pull-to-refresh.tsx @@ -66,9 +66,9 @@ const isTreeScrollable = (element: HTMLElement, direction: DIRECTION): boolean = interface PullToRefreshProps { isPullable?: boolean; onRefresh?: () => Promise | void; - refreshingContent?: JSX.Element | string; - pullingContent?: JSX.Element | string; - children: JSX.Element; + refreshingContent?: React.JSX.Element | string; + pullingContent?: React.JSX.Element | string; + children: React.JSX.Element; pullDownThreshold?: number; maxPullDownDistance?: number; resistance?: number; diff --git a/packages/pl-fe/src/components/scrollable-list.tsx b/packages/pl-fe/src/components/scrollable-list.tsx index 954a79170..72d7f26d3 100644 --- a/packages/pl-fe/src/components/scrollable-list.tsx +++ b/packages/pl-fe/src/components/scrollable-list.tsx @@ -32,13 +32,13 @@ type SavedScrollPosition = { // NOTE: It's crucial to space lists with **padding** instead of margin! // Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className // https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around -const Item: Components['Item'] = ({ context, ...rest }) => ( +const Item: Components['Item'] = ({ context, ...rest }) => (
); /** Custom Virtuoso List component for the outer container. */ // Ensure the className winds up here -const List: Components['List'] = React.forwardRef((props, ref) => { +const List: Components['List'] = React.forwardRef((props, ref) => { const { context, ...rest } = props; return
; }); @@ -177,7 +177,7 @@ const ScrollableList = React.forwardRef( }, []); /* Render an empty state instead of the scrollable list. */ - const renderEmpty = (): JSX.Element => { + const renderEmpty = (): React.JSX.Element => { return isLoading ? ( ) : emptyMessageText ? ( @@ -188,7 +188,7 @@ const ScrollableList = React.forwardRef( }; /** Render a single item. */ - const renderItem = (_i: number, element: JSX.Element): JSX.Element => { + const renderItem = (_i: number, element: React.JSX.Element): React.JSX.Element => { if (showPlaceholder) { return ; } else { diff --git a/packages/pl-fe/src/components/sidebar-navigation-link.tsx b/packages/pl-fe/src/components/sidebar-navigation-link.tsx index 298c913f7..f1fc2e2a6 100644 --- a/packages/pl-fe/src/components/sidebar-navigation-link.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation-link.tsx @@ -23,7 +23,10 @@ interface ISidebarNavigationLink extends Partial { /** Desktop sidebar navigation link. */ const SidebarNavigationLink = React.memo( React.forwardRef( - (props: ISidebarNavigationLink, ref: React.ForwardedRef): JSX.Element => { + ( + props: ISidebarNavigationLink, + ref: React.ForwardedRef, + ): React.JSX.Element => { const { icon, activeIcon, text, to, count, countMax, onClick, ...rest } = props; const matchRoute = useMatchRoute(); diff --git a/packages/pl-fe/src/components/status-action-button.tsx b/packages/pl-fe/src/components/status-action-button.tsx index 070ee98db..210eb012b 100644 --- a/packages/pl-fe/src/components/status-action-button.tsx +++ b/packages/pl-fe/src/components/status-action-button.tsx @@ -14,7 +14,7 @@ interface IStatusActionCounter { /** Action button numerical counter, eg "5" likes. */ const StatusActionCounter: React.FC = React.memo( - ({ count = 0 }): JSX.Element => { + ({ count = 0 }): React.JSX.Element => { const { demetricator } = useSettings(); return ( @@ -36,7 +36,7 @@ interface IStatusActionButton extends React.ButtonHTMLAttributes( - (props, ref): JSX.Element => { + (props, ref): React.JSX.Element => { const { icon, filledIcon, diff --git a/packages/pl-fe/src/components/status-list.tsx b/packages/pl-fe/src/components/status-list.tsx index b51dd8905..e6b835ce2 100644 --- a/packages/pl-fe/src/components/status-list.tsx +++ b/packages/pl-fe/src/components/status-list.tsx @@ -64,7 +64,7 @@ const StatusList: React.FC = ({ className, ...other }) => { - const node = useRef(null); + const node = useRef(null); const getFeaturedStatusCount = () => featuredStatusIds?.length ?? 0; diff --git a/packages/pl-fe/src/components/status-media.tsx b/packages/pl-fe/src/components/status-media.tsx index ab4e8abee..8dede2e7d 100644 --- a/packages/pl-fe/src/components/status-media.tsx +++ b/packages/pl-fe/src/components/status-media.tsx @@ -42,20 +42,20 @@ const StatusMedia: React.FC = ({ status, muted = false, onClick }) const size = status.media_attachments.length; const firstAttachment = status.media_attachments[0]; - let media: JSX.Element | null = null; + let media: React.JSX.Element | null = null; - const renderLoadingMediaGallery = (): JSX.Element => ( + const renderLoadingMediaGallery = (): React.JSX.Element => (
); - const renderLoadingVideoPlayer = (): JSX.Element => ( + const renderLoadingVideoPlayer = (): React.JSX.Element => (
); - const renderLoadingAudioPlayer = (): JSX.Element => ( + const renderLoadingAudioPlayer = (): React.JSX.Element => (
= ({ accountId, fallback }) => { diff --git a/packages/pl-fe/src/components/thumb-navigation-link.tsx b/packages/pl-fe/src/components/thumb-navigation-link.tsx index df620b1a6..ee450eee7 100644 --- a/packages/pl-fe/src/components/thumb-navigation-link.tsx +++ b/packages/pl-fe/src/components/thumb-navigation-link.tsx @@ -22,7 +22,7 @@ const ThumbNavigationLink: React.FC = ({ text, exact, ...props -}): JSX.Element => { +}): React.JSX.Element => { const { demetricator } = useSettings(); const matchRoute = useMatchRoute(); diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index b48b19556..b31dda931 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -29,7 +29,7 @@ const messages = defineMessages({ closeSidebar: { id: 'navigation.sidebar.close', defaultMessage: 'Close sidebar' }, }); -const ThumbNavigation: React.FC = React.memo((): JSX.Element => { +const ThumbNavigation: React.FC = React.memo((): React.JSX.Element => { const intl = useIntl(); const dispatch = useAppDispatch(); const { account } = useOwnAccount(); diff --git a/packages/pl-fe/src/components/ui/button/index.tsx b/packages/pl-fe/src/components/ui/button/index.tsx index cd644bb7a..7f70b5fa8 100644 --- a/packages/pl-fe/src/components/ui/button/index.tsx +++ b/packages/pl-fe/src/components/ui/button/index.tsx @@ -59,7 +59,7 @@ const Button = React.forwardRef( ...props }, ref: React.ForwardedRef, - ): JSX.Element => { + ): React.JSX.Element => { const body = text ?? children; const themeClass = useButtonStyles({ diff --git a/packages/pl-fe/src/components/ui/card.tsx b/packages/pl-fe/src/components/ui/card.tsx index a37182519..cd65af238 100644 --- a/packages/pl-fe/src/components/ui/card.tsx +++ b/packages/pl-fe/src/components/ui/card.tsx @@ -28,7 +28,7 @@ const Card = React.forwardRef( ( { children, variant = 'default', size = 'md', className, ...filteredProps }, ref, - ): JSX.Element => ( + ): React.JSX.Element => (
= ({ children, backHref, onBackClick, -}): JSX.Element => { +}): React.JSX.Element => { const intl = useIntl(); const renderBackButton = () => { @@ -99,7 +99,7 @@ interface ICardTitle { } /** A card's title. */ -const CardTitle: React.FC = ({ title, truncate = true }): JSX.Element => ( +const CardTitle: React.FC = ({ title, truncate = true }): React.JSX.Element => (

{title}

@@ -113,7 +113,7 @@ interface ICardBody { } /** A card's body. */ -const CardBody: React.FC = ({ className, children }): JSX.Element => ( +const CardBody: React.FC = ({ className, children }): React.JSX.Element => (
{children}
diff --git a/packages/pl-fe/src/components/ui/column.tsx b/packages/pl-fe/src/components/ui/column.tsx index 9087b3444..8fde9b5c7 100644 --- a/packages/pl-fe/src/components/ui/column.tsx +++ b/packages/pl-fe/src/components/ui/column.tsx @@ -67,7 +67,7 @@ interface IColumn { } /** A backdrop for the main section of the UI. */ -const Column: React.FC = (props): JSX.Element => { +const Column: React.FC = (props): React.JSX.Element => { const { backHref, children, diff --git a/packages/pl-fe/src/components/ui/emoji.tsx b/packages/pl-fe/src/components/ui/emoji.tsx index b888a150e..bb66fae34 100644 --- a/packages/pl-fe/src/components/ui/emoji.tsx +++ b/packages/pl-fe/src/components/ui/emoji.tsx @@ -16,7 +16,7 @@ interface IEmoji extends Pick< } /** A single emoji image. */ -const Emoji: React.FC = (props): JSX.Element | null => { +const Emoji: React.FC = (props): React.JSX.Element | null => { const { disableUserProvidedMedia, systemEmojiFont } = useSettings(); const { emoji, alt, src, staticSrc, noGroup, ...rest } = props; diff --git a/packages/pl-fe/src/components/ui/hstack.tsx b/packages/pl-fe/src/components/ui/hstack.tsx index 581cc2e16..26b9ed84d 100644 --- a/packages/pl-fe/src/components/ui/hstack.tsx +++ b/packages/pl-fe/src/components/ui/hstack.tsx @@ -52,7 +52,7 @@ interface IHStack extends Pick< /** Whether to let the flexbox grow. */ grow?: boolean; /** HTML element to use for container. */ - element?: React.ComponentType | keyof JSX.IntrinsicElements; + element?: React.ComponentType | keyof React.JSX.IntrinsicElements; /** Whether to let the flexbox wrap onto multiple lines. */ wrap?: boolean; } diff --git a/packages/pl-fe/src/components/ui/icon-button.tsx b/packages/pl-fe/src/components/ui/icon-button.tsx index 1bbb1e94c..56c5f5dbc 100644 --- a/packages/pl-fe/src/components/ui/icon-button.tsx +++ b/packages/pl-fe/src/components/ui/icon-button.tsx @@ -21,7 +21,7 @@ interface IIconButton extends React.ButtonHTMLAttributes { /** A clickable icon. */ const IconButton = React.forwardRef( - (props: IIconButton, ref: React.ForwardedRef): JSX.Element => { + (props: IIconButton, ref: React.ForwardedRef): React.JSX.Element => { const { src, className, iconClassName, text, theme = 'seamless', ...filteredProps } = props; const Component = (props.href ? 'a' : 'button') as 'button'; diff --git a/packages/pl-fe/src/components/ui/icon.tsx b/packages/pl-fe/src/components/ui/icon.tsx index d36f9abe4..7ae0ec207 100644 --- a/packages/pl-fe/src/components/ui/icon.tsx +++ b/packages/pl-fe/src/components/ui/icon.tsx @@ -29,7 +29,7 @@ const Icon: React.FC = React.forwardRef( ( { src, alt, count, size, countMax, containerClassName, title, ...filteredProps }, ref, - ): JSX.Element => ( + ): React.JSX.Element => (
; /** The content of the popover */ content: React.ReactNode; /** Should we remove padding on the Popover */ diff --git a/packages/pl-fe/src/components/ui/stack.tsx b/packages/pl-fe/src/components/ui/stack.tsx index ccdf1fa84..2c23cb6b8 100644 --- a/packages/pl-fe/src/components/ui/stack.tsx +++ b/packages/pl-fe/src/components/ui/stack.tsx @@ -39,7 +39,7 @@ interface IStack extends React.HTMLAttributes { /** Whether to let the flexbox grow. */ grow?: boolean; /** HTML element to use for container. */ - element?: React.ComponentType | keyof JSX.IntrinsicElements; + element?: React.ComponentType | keyof React.JSX.IntrinsicElements; } /** Vertical stack of child elements. */ diff --git a/packages/pl-fe/src/components/ui/streamfield.tsx b/packages/pl-fe/src/components/ui/streamfield.tsx index cb01db3d0..823ff7c01 100644 --- a/packages/pl-fe/src/components/ui/streamfield.tsx +++ b/packages/pl-fe/src/components/ui/streamfield.tsx @@ -57,8 +57,8 @@ const Streamfield: React.FC = ({ }) => { const intl = useIntl(); - const dragItem = useRef(); - const dragOverItem = useRef(); + const dragItem = useRef(null); + const dragOverItem = useRef(null); const handleDragStart = (i: number) => () => { dragItem.current = i; diff --git a/packages/pl-fe/src/components/ui/svg-icon.tsx b/packages/pl-fe/src/components/ui/svg-icon.tsx index 1f66a900b..299b4ee9f 100644 --- a/packages/pl-fe/src/components/ui/svg-icon.tsx +++ b/packages/pl-fe/src/components/ui/svg-icon.tsx @@ -19,7 +19,7 @@ const SvgIcon: React.FC = ({ size = 24, className, ...filteredProps -}): JSX.Element => { +}): React.JSX.Element => { const loader = ( = ({ children, ...rest }) => { const [activeRect, setActiveRect] = React.useState(null); - const ref = React.useRef(); + const ref = React.useRef(null); const rect = useRect(ref); // @ts-ignore @@ -80,7 +80,7 @@ const AnimatedTab: React.FC = ({ index, ...props }) => { const isSelected: boolean = selectedIndex === index; // measure the size of our element, only listen to rect if active - const ref = React.useRef(); + const ref = React.useRef(null); const rect = useRect(ref, { observe: isSelected }); // get the style changing function from context diff --git a/packages/pl-fe/src/components/ui/tooltip.tsx b/packages/pl-fe/src/components/ui/tooltip.tsx index 452f7743c..08b55dc74 100644 --- a/packages/pl-fe/src/components/ui/tooltip.tsx +++ b/packages/pl-fe/src/components/ui/tooltip.tsx @@ -13,7 +13,7 @@ import React, { useRef, useState } from 'react'; interface ITooltip { /** Element to display the tooltip around. */ - children: React.ReactElement; + children: React.ReactElement; /** Text to display in the tooltip. */ text: string; /** If disabled, it will render the children without wrapping them. */ diff --git a/packages/pl-fe/src/components/ui/widget.tsx b/packages/pl-fe/src/components/ui/widget.tsx index 44b2d7306..ac60ac39c 100644 --- a/packages/pl-fe/src/components/ui/widget.tsx +++ b/packages/pl-fe/src/components/ui/widget.tsx @@ -12,7 +12,7 @@ interface IWidget { actionIcon?: string; /** Text for the action. */ actionTitle?: string; - action?: JSX.Element; + action?: React.JSX.Element; children?: React.ReactNode; className?: string; } @@ -26,7 +26,7 @@ const Widget: React.FC = ({ actionTitle, action, className, -}): JSX.Element => { +}): React.JSX.Element => { const widgetId = useMemo(() => crypto.randomUUID(), []); return ( diff --git a/packages/pl-fe/src/features/admin/components/counter.tsx b/packages/pl-fe/src/features/admin/components/counter.tsx index 1fcefc453..d9ea26092 100644 --- a/packages/pl-fe/src/features/admin/components/counter.tsx +++ b/packages/pl-fe/src/features/admin/components/counter.tsx @@ -31,7 +31,7 @@ type ICounter = { measure: AdminMeasureKey; startAt: string; endAt: string; - label: JSX.Element | string; + label: React.JSX.Element | string; params?: AdminGetMeasuresParams; target?: string; } & (LinkOptions | {}); diff --git a/packages/pl-fe/src/features/admin/components/dimension.tsx b/packages/pl-fe/src/features/admin/components/dimension.tsx index b98707c28..5604aed8a 100644 --- a/packages/pl-fe/src/features/admin/components/dimension.tsx +++ b/packages/pl-fe/src/features/admin/components/dimension.tsx @@ -10,7 +10,7 @@ interface IDimension { dimension: AdminDimensionKey; startAt: string; endAt: string; - label?: JSX.Element; + label?: React.JSX.Element; params: AdminGetDimensionsParams; } diff --git a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx index 3b22fbe02..294ee6816 100644 --- a/packages/pl-fe/src/features/chats/components/chat-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-message-list.tsx @@ -79,7 +79,7 @@ interface IChatMessageList { const ChatMessageList: React.FC = React.memo(({ chat }) => { const intl = useIntl(); - const node = useRef(null); + const node = useRef(null); const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20); const markChatAsRead = useMarkChatAsRead(chat.id); diff --git a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx index da16251a3..b359708d9 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx @@ -71,7 +71,6 @@ const ChatSearch: React.FC = ({ isMainPage = false }) => { handleClickOnSearchResult.mutate(id); clearValue(); }} - parentRef={parentRef} /> ); } else if (hasSearchValue && !hasSearchResults && !isFetching) { diff --git a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx index e0e535693..dc329002a 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/results.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/results.tsx @@ -16,10 +16,9 @@ import type { Account } from 'pl-api'; interface IResults { accountSearchResult: ReturnType; onSelect(id: string): void; - parentRef: React.RefObject; } -const Results = ({ accountSearchResult, onSelect, parentRef }: IResults) => { +const Results = ({ accountSearchResult, onSelect }: IResults) => { const { data: accountIds = [], isFetching, hasNextPage, fetchNextPage } = accountSearchResult; const accounts = useAppSelector((state) => selectAccounts(state, accountIds)); diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx index d598eec9a..591355a6c 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-window.tsx @@ -25,7 +25,7 @@ const LinkWrapper = ({ enabled, children, ...rest -}: LinkProps & { enabled: boolean; children: React.ReactNode }): JSX.Element => { +}: LinkProps & { enabled: boolean; children: React.ReactNode }): React.JSX.Element => { if (!enabled) { return <>{children}; } diff --git a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx index fd09f64f1..e702866ee 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx @@ -100,7 +100,7 @@ const ShoutboxMessage: React.FC = ({ message, isMyMessage }) = /** Scrollable list of shoutbox messages. */ const ShoutboxMessageList: React.FC = () => { - const node = useRef(null); + const node = useRef(null); const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20); const me = useAppSelector((state) => state.me); diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index 04bf9e41f..a5be1a385 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -132,7 +132,7 @@ interface IComposeForm { id: ID extends 'default' ? never : ID; shouldCondense?: boolean; autoFocus?: boolean; - clickableAreaRef?: React.RefObject; + clickableAreaRef?: React.RefObject; event?: string; group?: string; withAvatar?: boolean; @@ -363,7 +363,7 @@ const ComposeForm = ({
); - let publishText: string | JSX.Element = ''; + let publishText: string | React.JSX.Element = ''; let publishIcon: string | undefined = undefined; if (isEditing) { diff --git a/packages/pl-fe/src/features/compose/components/upload-form.tsx b/packages/pl-fe/src/features/compose/components/upload-form.tsx index 4ae9ddcd7..7a46fe5df 100644 --- a/packages/pl-fe/src/features/compose/components/upload-form.tsx +++ b/packages/pl-fe/src/features/compose/components/upload-form.tsx @@ -21,8 +21,8 @@ const UploadForm: React.FC = ({ composeId, onSubmit }) => { const mediaIds = mediaAttachments.map((item) => item.id); - const dragItem = useRef(); - const dragOverItem = useRef(); + const dragItem = useRef(null); + const dragOverItem = useRef(null); const handleDragStart = useCallback( (id: string) => { diff --git a/packages/pl-fe/src/features/compose/editor/nodes/emoji-node.tsx b/packages/pl-fe/src/features/compose/editor/nodes/emoji-node.tsx index e51df96b3..eefa8e9f6 100644 --- a/packages/pl-fe/src/features/compose/editor/nodes/emoji-node.tsx +++ b/packages/pl-fe/src/features/compose/editor/nodes/emoji-node.tsx @@ -15,7 +15,7 @@ type SerializedEmojiNode = Spread< SerializedLexicalNode >; -class EmojiNode extends DecoratorNode { +class EmojiNode extends DecoratorNode { __emoji: Emoji; static getType(): 'emoji' { @@ -74,7 +74,7 @@ class EmojiNode extends DecoratorNode { } } - decorate(): JSX.Element { + decorate(): React.JSX.Element { const emoji = this.__emoji; if (isNativeEmoji(emoji)) { return ( diff --git a/packages/pl-fe/src/features/compose/editor/nodes/image-component.tsx b/packages/pl-fe/src/features/compose/editor/nodes/image-component.tsx index 84836eb29..3f501c0e1 100644 --- a/packages/pl-fe/src/features/compose/editor/nodes/image-component.tsx +++ b/packages/pl-fe/src/features/compose/editor/nodes/image-component.tsx @@ -74,7 +74,7 @@ const LazyImage = ({ className: string | null; imageRef: { current: null | HTMLImageElement }; src: string; -}): JSX.Element => { +}): React.JSX.Element => { useSuspenseImage(src); return ( { +}): React.JSX.Element => { const intl = useIntl(); const { openModal } = useModalsActions(); const { missingDescriptionModal } = useSettings(); diff --git a/packages/pl-fe/src/features/compose/editor/nodes/image-node.tsx b/packages/pl-fe/src/features/compose/editor/nodes/image-node.tsx index 735c6e6e4..8502a1896 100644 --- a/packages/pl-fe/src/features/compose/editor/nodes/image-node.tsx +++ b/packages/pl-fe/src/features/compose/editor/nodes/image-node.tsx @@ -43,7 +43,7 @@ type SerializedImageNode = Spread< SerializedLexicalNode >; -class ImageNode extends DecoratorNode { +class ImageNode extends DecoratorNode { __src: string; __altText: string; @@ -127,7 +127,7 @@ class ImageNode extends DecoratorNode { } } - decorate(): JSX.Element { + decorate(): React.JSX.Element { return ( // diff --git a/packages/pl-fe/src/features/compose/editor/nodes/mention-node.tsx b/packages/pl-fe/src/features/compose/editor/nodes/mention-node.tsx index 3e6603249..b59e93b95 100644 --- a/packages/pl-fe/src/features/compose/editor/nodes/mention-node.tsx +++ b/packages/pl-fe/src/features/compose/editor/nodes/mention-node.tsx @@ -21,7 +21,7 @@ type SerializedMentionNode = Spread< SerializedLexicalNode >; -class MentionNode extends DecoratorNode { +class MentionNode extends DecoratorNode { __mention: MentionEntity; static getType(): string { @@ -70,7 +70,7 @@ class MentionNode extends DecoratorNode { return true; } - decorate(): JSX.Element { + decorate(): React.JSX.Element { return ; } } diff --git a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx index a6f4ce69c..5de82cb0e 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -61,7 +61,7 @@ type Resolution = { type MenuRenderFn = ( anchorElementRef: MutableRefObject, -) => ReactPortal | JSX.Element | null; +) => ReactPortal | React.JSX.Element | null; const tryToPositionRange = (leadOffset: number, range: Range): boolean => { const domSelection = window.getSelection(); @@ -197,7 +197,7 @@ const LexicalPopoverMenu = ({ }: { anchorElementRef: MutableRefObject; menuRenderFn: MenuRenderFn; -}): JSX.Element | null => menuRenderFn(anchorElementRef); +}): React.JSX.Element | null => menuRenderFn(anchorElementRef); const useMenuAnchorRef = ( resolution: Resolution | null, @@ -272,7 +272,7 @@ const AutosuggestPlugin = ({ composeId, suggestionsHidden, setSuggestionsHidden, -}: AutosuggestPluginProps): JSX.Element | null => { +}: AutosuggestPluginProps): React.JSX.Element | null => { const { rememberEmojiUse } = useSettingsStoreActions(); const { suggestions } = useCompose(composeId); const dispatch = useAppDispatch(); @@ -349,7 +349,7 @@ const AutosuggestPlugin = ({ }; const renderSuggestion = (suggestion: AutoSuggestion, i: number) => { - let inner: string | JSX.Element; + let inner: string | React.JSX.Element; let key: React.Key; if (typeof suggestion === 'object') { diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx index e172b7379..6676f4208 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx @@ -110,7 +110,7 @@ const BlockTypeFloatingToolbar = ({ }: { editor: LexicalEditor; anchorElem: HTMLElement; -}): JSX.Element => { +}): React.JSX.Element => { const intl = useIntl(); const popupCharStylesEditorRef = useRef(null); const { composeAllowInlineImages } = useFeatures(); @@ -231,7 +231,7 @@ const BlockTypeFloatingToolbar = ({ const useFloatingBlockTypeToolbar = ( editor: LexicalEditor, anchorElem: HTMLElement, -): JSX.Element | null => { +): React.JSX.Element | null => { const [isEmptyBlock, setIsEmptyBlock] = useState(false); const updatePopup = useCallback(() => { @@ -300,7 +300,7 @@ const FloatingBlockTypeToolbarPlugin = ({ anchorElem = document.body, }: { anchorElem?: HTMLElement; -}): JSX.Element | null => { +}): React.JSX.Element | null => { const [editor] = useLexicalComposerContext(); return useFloatingBlockTypeToolbar(editor, anchorElem); }; diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx index 8dcc2afb4..cf481b3af 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx @@ -32,7 +32,7 @@ const FloatingLinkEditor = ({ }: { editor: LexicalEditor; anchorElem: HTMLElement; -}): JSX.Element => { +}): React.JSX.Element => { const editorRef = useRef(null); const inputRef = useRef(null); const [linkUrl, setLinkUrl] = useState(''); @@ -231,7 +231,7 @@ const FloatingLinkEditor = ({ const useFloatingLinkEditorToolbar = ( editor: LexicalEditor, anchorElem: HTMLElement, -): JSX.Element | null => { +): React.JSX.Element | null => { const [activeEditor, setActiveEditor] = useState(editor); const [isLink, setIsLink] = useState(false); @@ -272,7 +272,7 @@ const FloatingLinkEditorPlugin = ({ anchorElem = document.body, }: { anchorElem?: HTMLElement; -}): JSX.Element | null => { +}): React.JSX.Element | null => { const [editor] = useLexicalComposerContext(); return useFloatingLinkEditorToolbar(editor, anchorElem); }; diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx index 08208d9e3..55297e361 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx @@ -288,7 +288,7 @@ const TextFormatFloatingToolbar = ({ isLink: boolean; isStrikethrough: boolean; isUnderline: boolean; -}): JSX.Element => { +}): React.JSX.Element => { const intl = useIntl(); const popupCharStylesEditorRef = useRef(null); @@ -437,7 +437,7 @@ const TextFormatFloatingToolbar = ({ const useFloatingTextFormatToolbar = ( editor: LexicalEditor, anchorElem: HTMLElement, -): JSX.Element | null => { +): React.JSX.Element | null => { const [blockType, setBlockType] = useState('paragraph'); const [isText, setIsText] = useState(false); const [isLink, setIsLink] = useState(false); @@ -567,7 +567,7 @@ const FloatingTextFormatToolbarPlugin = ({ anchorElem = document.body, }: { anchorElem?: HTMLElement; -}): JSX.Element | null => { +}): React.JSX.Element | null => { const [editor] = useLexicalComposerContext(); return useFloatingTextFormatToolbar(editor, anchorElem); }; diff --git a/packages/pl-fe/src/features/compose/editor/plugins/link-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/link-plugin.tsx index c5096e6a6..91712fc82 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/link-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/link-plugin.tsx @@ -18,6 +18,6 @@ const validateUrl = (url: string): boolean => { return url === 'https://' || urlRegExp.test(url); }; -const LinkPlugin = (): JSX.Element => ; +const LinkPlugin = (): React.JSX.Element => ; export { LinkPlugin as default }; diff --git a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx index d7a8cdb12..b55f88d00 100644 --- a/packages/pl-fe/src/features/conversations/components/conversations-list.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversations-list.tsx @@ -12,7 +12,7 @@ import Conversation from './conversation'; import type { VirtuosoHandle } from 'react-virtuoso'; const ConversationsList: React.FC = () => { - const ref = useRef(null); + const ref = useRef(null); const { conversations, isLoading, hasNextPage, isFetching, fetchNextPage } = useConversations(); diff --git a/packages/pl-fe/src/features/crypto-donate/components/crypto-address.tsx b/packages/pl-fe/src/features/crypto-donate/components/crypto-address.tsx index ad7a6d3f1..44371dbba 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/crypto-address.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/crypto-address.tsx @@ -17,7 +17,7 @@ interface ICryptoAddress { note?: string; } -const CryptoAddress: React.FC = (props): JSX.Element => { +const CryptoAddress: React.FC = (props): React.JSX.Element => { const { address, ticker, note } = props; const { openModal } = useModalsActions(); diff --git a/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx b/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx index d1403f5c3..b66147d6a 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/crypto-donate-panel.tsx @@ -20,7 +20,9 @@ interface ICryptoDonatePanel { limit: number; } -const CryptoDonatePanel: React.FC = ({ limit = 3 }): JSX.Element | null => { +const CryptoDonatePanel: React.FC = ({ + limit = 3, +}): React.JSX.Element | null => { const intl = useIntl(); const navigate = useNavigate(); const instance = useInstance(); diff --git a/packages/pl-fe/src/features/crypto-donate/components/crypto-icon.tsx b/packages/pl-fe/src/features/crypto-donate/components/crypto-icon.tsx index 7dcb3b34e..09cbb3a92 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/crypto-icon.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/crypto-icon.tsx @@ -23,7 +23,7 @@ const CryptoIcon: React.FC = ({ title, className, imgClassName, -}): JSX.Element => ( +}): React.JSX.Element => (
{title
diff --git a/packages/pl-fe/src/features/crypto-donate/components/detailed-crypto-address.tsx b/packages/pl-fe/src/features/crypto-donate/components/detailed-crypto-address.tsx index a9cedc6cb..6b156de7b 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/detailed-crypto-address.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/detailed-crypto-address.tsx @@ -17,7 +17,7 @@ const DetailedCryptoAddress: React.FC = ({ address, ticker, note, -}): JSX.Element => { +}): React.JSX.Element => { const title = getTitle(ticker); return ( diff --git a/packages/pl-fe/src/features/crypto-donate/components/lightning-address.tsx b/packages/pl-fe/src/features/crypto-donate/components/lightning-address.tsx index 36a7d46d2..9ff1abe58 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/lightning-address.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/lightning-address.tsx @@ -11,7 +11,7 @@ interface ILightningAddress { address: string; } -const LightningAddress: React.FC = (props): JSX.Element => { +const LightningAddress: React.FC = (props): React.JSX.Element => { const { address } = props; return ( diff --git a/packages/pl-fe/src/features/crypto-donate/components/site-wallet.tsx b/packages/pl-fe/src/features/crypto-donate/components/site-wallet.tsx index dba4cf762..75e9f7800 100644 --- a/packages/pl-fe/src/features/crypto-donate/components/site-wallet.tsx +++ b/packages/pl-fe/src/features/crypto-donate/components/site-wallet.tsx @@ -9,7 +9,7 @@ interface ISiteWallet { limit?: number; } -const SiteWallet: React.FC = ({ limit }): JSX.Element => { +const SiteWallet: React.FC = ({ limit }): React.JSX.Element => { const { cryptoAddresses } = useFrontendConfig(); const addresses = typeof limit === 'number' ? cryptoAddresses.slice(0, limit) : cryptoAddresses; diff --git a/packages/pl-fe/src/features/emoji/containers/emoji-picker-dropdown-container.tsx b/packages/pl-fe/src/features/emoji/containers/emoji-picker-dropdown-container.tsx index 290a301ef..b7df165ff 100644 --- a/packages/pl-fe/src/features/emoji/containers/emoji-picker-dropdown-container.tsx +++ b/packages/pl-fe/src/features/emoji/containers/emoji-picker-dropdown-container.tsx @@ -17,7 +17,7 @@ interface IEmojiPickerDropdownContainer extends Pick< IEmojiPickerDropdown, 'onPickEmoji' | 'condensed' | 'withCustom' > { - children?: JSX.Element; + children?: React.JSX.Element; theme?: 'default' | 'inverse'; } diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index 8402d1823..3607ffd71 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -58,7 +58,7 @@ const notificationForScreenReader = (intl: IntlShape, message: string, timestamp const buildLink = ( account: Pick, -): JSX.Element => ( +): React.JSX.Element => ( ; } -const StatusInteractionBar: React.FC = ({ status }): JSX.Element | null => { +const StatusInteractionBar: React.FC = ({ + status, +}): React.JSX.Element | null => { const { openModal } = useModalsActions(); const features = useFeatures(); const { account } = status; diff --git a/packages/pl-fe/src/features/status/components/thread-status.tsx b/packages/pl-fe/src/features/status/components/thread-status.tsx index a922540a7..98f9c37d7 100644 --- a/packages/pl-fe/src/features/status/components/thread-status.tsx +++ b/packages/pl-fe/src/features/status/components/thread-status.tsx @@ -17,7 +17,7 @@ interface IThreadStatus { } /** Status with reply-connector in threads. */ -const ThreadStatus: React.FC = (props): JSX.Element => { +const ThreadStatus: React.FC = (props): React.JSX.Element => { const { id, focusedStatusId } = props; const replyToId = useReplyToId(id); @@ -33,7 +33,7 @@ const ThreadStatus: React.FC = (props): JSX.Element => { ); } - const renderConnector = (): JSX.Element | null => { + const renderConnector = (): React.JSX.Element | null => { if (props.linear) return null; const isConnectedTop = replyToId && replyToId !== focusedStatusId; diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index ca87abc40..402eca2bc 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -73,7 +73,7 @@ const Thread = ({ const node = useRef(null); const statusRef = useRef(null); - const scroller = useRef(null); + const scroller = useRef(null); const handleFavouriteClick = (status: SelectedStatus) => { if (status.favourited) unfavouriteStatus(); diff --git a/packages/pl-fe/src/features/ui/components/hotkeys.tsx b/packages/pl-fe/src/features/ui/components/hotkeys.tsx index fa583b890..0dbe1eb96 100644 --- a/packages/pl-fe/src/features/ui/components/hotkeys.tsx +++ b/packages/pl-fe/src/features/ui/components/hotkeys.tsx @@ -267,7 +267,7 @@ interface IHotkeys extends React.HTMLAttributes { */ focusable?: boolean; children: React.ReactNode; - element?: React.ComponentType | keyof JSX.IntrinsicElements; + element?: React.ComponentType | keyof React.JSX.IntrinsicElements; } /** diff --git a/packages/pl-fe/src/features/ui/components/link-footer.tsx b/packages/pl-fe/src/features/ui/components/link-footer.tsx index d91497c4c..2a982e8a5 100644 --- a/packages/pl-fe/src/features/ui/components/link-footer.tsx +++ b/packages/pl-fe/src/features/ui/components/link-footer.tsx @@ -9,7 +9,7 @@ const messages = defineMessages({ meow: { id: 'footer.meow', defaultMessage: 'meow :3 {emoji}' }, }); -const LinkFooter: React.FC = (): JSX.Element => { +const LinkFooter: React.FC = (): React.JSX.Element => { const intl = useIntl(); const frontendConfig = useFrontendConfig(); diff --git a/packages/pl-fe/src/features/ui/components/panels/birthday-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/birthday-panel.tsx index 8a0443573..f35c2f381 100644 --- a/packages/pl-fe/src/features/ui/components/panels/birthday-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/birthday-panel.tsx @@ -31,7 +31,7 @@ const BirthdayPanel = ({ limit }: IBirthdayPanel) => { const { data: birthdays = [] } = useBirthdayReminders(month, day); const birthdaysToRender = birthdays.slice(0, limit); - const timeout = useRef(); + const timeout = useRef(null); React.useEffect(() => { const updateTimeout = () => { diff --git a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx index 5af31a674..9e29eb81b 100644 --- a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx @@ -25,8 +25,8 @@ const messages = defineMessages({ interface IUserPanel { accountId: string; - action?: JSX.Element; - badges?: JSX.Element[]; + action?: React.JSX.Element; + badges?: React.JSX.Element[]; domain?: string; } diff --git a/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx b/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx index 1bfc2f477..150491d0d 100644 --- a/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx @@ -31,7 +31,7 @@ interface IProfileDropdown { type IMenuItem = { text: string | React.ReactElement | null; linkOptions?: LinkOptions; - toggle?: JSX.Element; + toggle?: React.JSX.Element; icon?: string; action?: (event: React.MouseEvent) => void; }; diff --git a/packages/pl-fe/src/features/ui/components/zoomable-image.tsx b/packages/pl-fe/src/features/ui/components/zoomable-image.tsx index 96785fd6a..68a47412f 100644 --- a/packages/pl-fe/src/features/ui/components/zoomable-image.tsx +++ b/packages/pl-fe/src/features/ui/components/zoomable-image.tsx @@ -116,7 +116,7 @@ const ZoomableImage: React.FC = ({ const containerRef = useRef(null); const imageRef = useRef(null); - const doubleClickTimeoutRef = useRef | null>(); + const doubleClickTimeoutRef = useRef | null>(null); const zoomMatrixRef = useRef(null); const [style, api] = useSpring(() => ({ diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index ec6589bc8..ce81482be 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -1560,7 +1560,7 @@ const routeTree = rootRoute.addChildren([ ...redirectRoutes, ]); -const FallbackLayout: React.FC<{ children: JSX.Element }> = ({ children }) => ( +const FallbackLayout: React.FC<{ children: React.JSX.Element }> = ({ children }) => ( <> {children} diff --git a/packages/pl-fe/src/hooks/forms/use-image-field.ts b/packages/pl-fe/src/hooks/forms/use-image-field.ts index 1ffa40da4..0395bfc54 100644 --- a/packages/pl-fe/src/hooks/forms/use-image-field.ts +++ b/packages/pl-fe/src/hooks/forms/use-image-field.ts @@ -16,7 +16,7 @@ interface UseImageFieldOpts { const useImageField = (opts: UseImageFieldOpts = {}) => { const { stripMetadata } = useSettings(); - const [file, setFile] = useState(); + const [file, setFile] = useState(null); const src = usePreview(file) ?? (file === null ? undefined : opts.preview); const onChange = async (files: FileList | null) => { diff --git a/packages/pl-fe/src/hooks/use-dragged-files.ts b/packages/pl-fe/src/hooks/use-dragged-files.ts index ea43676c3..c9e3452a9 100644 --- a/packages/pl-fe/src/hooks/use-dragged-files.ts +++ b/packages/pl-fe/src/hooks/use-dragged-files.ts @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; /** Controls the state of files being dragged over a node. */ const useDraggedFiles = ( - node: React.RefObject, + node: React.RefObject, onDrop?: (files: FileList) => void, ) => { const [isDragging, setIsDragging] = useState(false); diff --git a/packages/pl-fe/src/hooks/use-long-press.ts b/packages/pl-fe/src/hooks/use-long-press.ts index fb09fef28..0cf43ef0f 100644 --- a/packages/pl-fe/src/hooks/use-long-press.ts +++ b/packages/pl-fe/src/hooks/use-long-press.ts @@ -53,7 +53,7 @@ const useLongPress = (callback: (e: Event) => void, options: LongPressOptions = } = options; const isLongPressActive = React.useRef(false); const isPressed = React.useRef(false); - const timerId = React.useRef(); + const timerId = React.useRef(null); let startY: number; return React.useMemo(() => { diff --git a/packages/pl-fe/src/hooks/use-previous.ts b/packages/pl-fe/src/hooks/use-previous.ts index 443e60d00..1c3f3629f 100644 --- a/packages/pl-fe/src/hooks/use-previous.ts +++ b/packages/pl-fe/src/hooks/use-previous.ts @@ -3,13 +3,13 @@ import { useRef, useEffect } from 'react'; /** Get the last version of this value. */ // https://usehooks.com/usePrevious/ const usePrevious = (value: T): T | undefined => { - const ref = useRef(); + const ref = useRef(null); useEffect(() => { ref.current = value; }, [value]); - return ref.current; + return ref.current || undefined; }; export { usePrevious }; diff --git a/packages/pl-fe/src/modals/dropdown-menu-modal.tsx b/packages/pl-fe/src/modals/dropdown-menu-modal.tsx index fe38e9c81..964e58d4c 100644 --- a/packages/pl-fe/src/modals/dropdown-menu-modal.tsx +++ b/packages/pl-fe/src/modals/dropdown-menu-modal.tsx @@ -7,7 +7,7 @@ import type { BaseModalProps } from '@/features/ui/components/modal-root'; interface DropdownMenuModalProps { /** The element initiating opening the modal. */ element?: HTMLElement; - content?: JSX.Element; + content?: React.JSX.Element; } const DropdownMenuModal: React.FC = ({ diff --git a/packages/pl-fe/src/modals/event-map-modal.tsx b/packages/pl-fe/src/modals/event-map-modal.tsx index a2c19eb8d..a9fdab2a0 100644 --- a/packages/pl-fe/src/modals/event-map-modal.tsx +++ b/packages/pl-fe/src/modals/event-map-modal.tsx @@ -29,7 +29,7 @@ const EventMapModal: React.FC = ({ onClose, const status = useAppSelector((state) => getStatus(state, { id: statusId }))!; const location = status.event!.location!; - const map = useRef(); + const map = useRef(null); useEffect(() => { const latlng: [number, number] = [location.latitude!, location.longitude!]; diff --git a/packages/pl-fe/src/modals/hotkeys-modal.tsx b/packages/pl-fe/src/modals/hotkeys-modal.tsx index 0da57ab25..72b3277a3 100644 --- a/packages/pl-fe/src/modals/hotkeys-modal.tsx +++ b/packages/pl-fe/src/modals/hotkeys-modal.tsx @@ -263,8 +263,8 @@ const HotkeysModal: React.FC = ({ onClose }) => { const columns = columnSizes.reduce< Array< Array<{ - key: JSX.Element; - label: JSX.Element; + key: React.JSX.Element; + label: React.JSX.Element; }> > >((prev, cur) => { diff --git a/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx b/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx index e51506b1f..92a452a0d 100644 --- a/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx +++ b/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx @@ -74,7 +74,7 @@ interface IInteractionRequestStatus { id: string; hasReply?: boolean; isReply?: boolean; - actions?: JSX.Element; + actions?: React.JSX.Element; } const InteractionRequestStatus: React.FC = ({ diff --git a/packages/pl-fe/src/pages/statuses/event-discussion.tsx b/packages/pl-fe/src/pages/statuses/event-discussion.tsx index dca9d28d0..729316972 100644 --- a/packages/pl-fe/src/pages/statuses/event-discussion.tsx +++ b/packages/pl-fe/src/pages/statuses/event-discussion.tsx @@ -36,7 +36,7 @@ const EventDiscussionPage: React.FC = () => { const [isLoaded, setIsLoaded] = useState(!!status); const node = useRef(null); - const scroller = useRef(null); + const scroller = useRef(null); const fetchData = () => dispatch(fetchStatusWithContext(statusId, intl)); @@ -105,7 +105,7 @@ const EventDiscussionPage: React.FC = () => { return ; } - const children: JSX.Element[] = []; + const children: React.JSX.Element[] = []; if (hasDescendants) { children.push(...renderChildren(descendantsIds)); diff --git a/packages/pl-fe/src/pages/utils/crypto-donate.tsx b/packages/pl-fe/src/pages/utils/crypto-donate.tsx index 52a792995..45d12ae56 100644 --- a/packages/pl-fe/src/pages/utils/crypto-donate.tsx +++ b/packages/pl-fe/src/pages/utils/crypto-donate.tsx @@ -11,7 +11,7 @@ const messages = defineMessages({ heading: { id: 'column.crypto_donate', defaultMessage: 'Donate cryptocurrency' }, }); -const CryptoDonatePage: React.FC = (): JSX.Element => { +const CryptoDonatePage: React.FC = (): React.JSX.Element => { const intl = useIntl(); const instance = useInstance(); diff --git a/packages/pl-fe/src/utils/scroll-utils.ts b/packages/pl-fe/src/utils/scroll-utils.ts index a2e07402b..c13159596 100644 --- a/packages/pl-fe/src/utils/scroll-utils.ts +++ b/packages/pl-fe/src/utils/scroll-utils.ts @@ -3,7 +3,7 @@ import type { VirtuosoHandle } from 'react-virtuoso'; const selectChild = ( index: number, - handle: React.RefObject, + handle: React.RefObject, node: ParentNode = document, count?: number, align?: 'start' | 'center' | 'end', From 8fd267e5d720bab5c17c4e07c428411f488c6c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 16:42:42 +0100 Subject: [PATCH 054/264] nicolium: add the old WIP timeline thing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../queries/timelines/use-home-timeline.ts | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 packages/pl-fe/src/queries/timelines/use-home-timeline.ts diff --git a/packages/pl-fe/src/queries/timelines/use-home-timeline.ts b/packages/pl-fe/src/queries/timelines/use-home-timeline.ts new file mode 100644 index 000000000..8bb6f24ca --- /dev/null +++ b/packages/pl-fe/src/queries/timelines/use-home-timeline.ts @@ -0,0 +1,163 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useState } from 'react'; + +import { importEntities } from '@/actions/importer'; +import { useTimelineStream } from '@/api/hooks/streaming/use-timeline-stream'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; + +import type { PaginatedResponse, Status } from 'pl-api'; + +type TimelineEntry = + | { + type: 'status'; + id: string; + rebloggedBy: Array; + isConnectedTop?: boolean; + isConnectedBottom?: boolean; + } + | { + type: 'pending-status'; + id: string; + } + | { + type: 'gap'; + } + | { + type: 'page-start'; + maxId?: string; + } + | { + type: 'page-end'; + minId?: string; + }; + +const processPage = ({ items: statuses, next }: PaginatedResponse) => { + const timelinePage: Array = []; + + // if (previous) timelinePage.push({ + // type: 'page-start', + // maxId: statuses.at(0)?.id, + // }); + + const processStatus = (status: Status) => { + if (timelinePage.some((entry) => entry.type === 'status' && entry.id === status.id)) + return false; + + let isConnectedTop = false; + const inReplyToId = (status.reblog || status).in_reply_to_id; + + if (inReplyToId) { + const foundStatus = statuses.find((status) => (status.reblog || status).id === inReplyToId); + + if (foundStatus) { + if (processStatus(foundStatus)) { + const lastEntry = timelinePage.at(-1); + // it's always of type status but doing this to satisfy ts + if (lastEntry?.type === 'status') lastEntry.isConnectedBottom = true; + + isConnectedTop = true; + } + } + } + + if (status.reblog) { + const existingEntry = timelinePage.find( + (entry) => entry.type === 'status' && entry.id === status.reblog!.id, + ); + + if (existingEntry?.type === 'status') { + existingEntry.rebloggedBy.push(status.account.id); + } else { + timelinePage.push({ + type: 'status', + id: status.reblog.id, + rebloggedBy: [status.account.id], + isConnectedTop, + }); + } + return true; + } + + timelinePage.push({ + type: 'status', + id: status.id, + rebloggedBy: [], + isConnectedTop, + }); + + return true; + }; + + for (const status of statuses) { + processStatus(status); + } + + if (next) + timelinePage.push({ + type: 'page-end', + minId: statuses.at(-1)?.id, + }); + + return timelinePage; +}; + +const useHomeTimeline = () => { + const client = useClient(); + const dispatch = useAppDispatch(); + const queryClient = useQueryClient(); + + useTimelineStream('home'); + + const [isLoading, setIsLoading] = useState(true); + + const queryKey = ['timelines', 'home']; + + const query = useQuery({ + queryKey, + queryFn: () => { + setIsLoading(true); + + return client.timelines + .homeTimeline() + .then((response) => { + dispatch(importEntities({ statuses: response.items })); + + return processPage(response); + }) + .catch(() => {}) + .finally(() => setIsLoading(false)); + }, + }); + + const handleLoadMore = (entry: TimelineEntry) => { + if (isLoading) return; + + setIsLoading(true); + if (entry.type !== 'page-end' && entry.type !== 'page-start') return; + + return client.timelines + .homeTimeline(entry.type === 'page-end' ? { max_id: entry.minId } : { min_id: entry.maxId }) + .then((response) => { + dispatch(importEntities({ statuses: response.items })); + + const timelinePage = processPage(response); + + queryClient.setQueryData>(['timelines', 'home'], (oldData) => { + if (!oldData) return timelinePage; + + const index = oldData.indexOf(entry); + return oldData.toSpliced(index, 1, ...timelinePage); + }); + }) + .catch(() => {}) + .finally(() => setIsLoading(false)); + }; + return { + ...query, + isLoading: isLoading, + handleLoadMore, + }; +}; + +export { useHomeTimeline, type TimelineEntry }; From 509ca5f1f49ade3950b70ac0d8d56b68503da0fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Tue, 24 Feb 2026 16:49:48 +0100 Subject: [PATCH 055/264] nicolium: rename MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/pages/utils/about.tsx | 2 +- .../pl-fe/src/queries/{pl-fe => frontend}/use-about-page.ts | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/pl-fe/src/queries/{pl-fe => frontend}/use-about-page.ts (100%) diff --git a/packages/pl-fe/src/pages/utils/about.tsx b/packages/pl-fe/src/pages/utils/about.tsx index 9116acfc7..efbb90ae2 100644 --- a/packages/pl-fe/src/pages/utils/about.tsx +++ b/packages/pl-fe/src/pages/utils/about.tsx @@ -6,7 +6,7 @@ import Card from '@/components/ui/card'; import { languages } from '@/features/preferences'; import { aboutRoute } from '@/features/ui/router'; import { useFrontendConfig } from '@/hooks/use-frontend-config'; -import { useAboutPage } from '@/queries/pl-fe/use-about-page'; +import { useAboutPage } from '@/queries/frontend/use-about-page'; import { useSettings } from '@/stores/settings'; interface IAbout { diff --git a/packages/pl-fe/src/queries/pl-fe/use-about-page.ts b/packages/pl-fe/src/queries/frontend/use-about-page.ts similarity index 100% rename from packages/pl-fe/src/queries/pl-fe/use-about-page.ts rename to packages/pl-fe/src/queries/frontend/use-about-page.ts From cac38a33d16abf9c1554d215ad8e0f626124e42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 13:49:46 +0100 Subject: [PATCH 056/264] nicolium: migrate accounts to react-query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/accounts.ts | 97 +------------------ packages/pl-fe/src/actions/auth.ts | 8 +- packages/pl-fe/src/actions/compose.ts | 44 ++++----- packages/pl-fe/src/actions/importer.ts | 19 ++-- packages/pl-fe/src/actions/me.ts | 4 +- packages/pl-fe/src/actions/moderation.tsx | 21 ++-- packages/pl-fe/src/actions/settings.ts | 2 +- .../api/hooks/accounts/use-account-lookup.ts | 51 ---------- .../src/api/hooks/accounts/use-account.ts | 81 ---------------- .../pl-fe/src/api/hooks/admin/use-suggest.ts | 52 ---------- .../pl-fe/src/api/hooks/admin/use-verify.ts | 57 ----------- .../src/components/account-hover-card.tsx | 11 +-- .../pl-fe/src/components/avatar-stack.tsx | 5 +- .../src/components/dropdown-navigation.tsx | 11 +-- .../src/components/hover-account-wrapper.tsx | 5 - .../pl-fe/src/components/search-input.tsx | 15 +-- .../src/components/sentry-feedback-form.tsx | 2 +- .../src/components/sidebar-navigation.tsx | 2 +- .../src/components/status-action-bar.tsx | 2 +- .../pl-fe/src/components/status-mention.tsx | 4 +- .../statuses/sensitive-content-overlay.tsx | 2 +- .../pl-fe/src/components/thumb-navigation.tsx | 2 +- packages/pl-fe/src/components/ui/modal.tsx | 2 +- .../src/containers/account-container.tsx | 4 +- packages/pl-fe/src/entity-store/actions.ts | 28 ------ packages/pl-fe/src/entity-store/entities.ts | 11 --- .../pl-fe/src/entity-store/hooks/types.ts | 22 ----- .../entity-store/hooks/use-entity-lookup.ts | 69 ------------- .../src/entity-store/hooks/use-entity.ts | 79 --------------- .../src/entity-store/hooks/use-transaction.ts | 23 ----- packages/pl-fe/src/entity-store/reducer.ts | 54 ----------- packages/pl-fe/src/entity-store/selectors.ts | 24 ----- packages/pl-fe/src/entity-store/types.ts | 28 ------ packages/pl-fe/src/entity-store/utils.ts | 18 ---- .../features/account/components/header.tsx | 2 +- .../src/features/admin/components/report.tsx | 4 +- .../components/registration-form.tsx | 10 +- .../pl-fe/src/features/birthdays/account.tsx | 4 +- .../chats/components/chat-list-shoutbox.tsx | 4 +- .../chats/components/chat-search/results.tsx | 5 +- .../components/chats-page-settings.tsx | 2 +- .../components/shoutbox-message-list.tsx | 4 +- .../components/autosuggest-account.tsx | 4 +- .../containers/preview-compose-container.tsx | 2 +- .../compose/containers/warning-container.tsx | 11 +-- .../editor/plugins/autosuggest-plugin.tsx | 7 +- .../conversations/components/conversation.tsx | 4 +- .../src/features/draft-statuses/builder.tsx | 8 +- .../components/draft-status.tsx | 11 ++- .../event/components/event-header.tsx | 2 +- .../components/group-member-list-item.tsx | 4 +- .../src/features/reply-mentions/account.tsx | 13 +-- .../features/scheduled-statuses/builder.tsx | 9 +- .../components/scheduled-status.tsx | 10 +- .../settings/components/messages-settings.tsx | 2 +- .../panels/instance-moderation-panel.tsx | 2 +- .../ui/components/panels/user-panel.tsx | 4 +- .../features/ui/components/pending-status.tsx | 6 +- .../ui/components/profile-dropdown.tsx | 18 +--- .../components/profile-familiar-followers.tsx | 4 +- packages/pl-fe/src/features/ui/index.tsx | 2 +- .../pl-fe/src/features/ui/router/index.tsx | 2 +- .../src/features/ui/util/global-hotkeys.tsx | 2 +- .../ui/util/pending-status-builder.ts | 11 ++- packages/pl-fe/src/hooks/use-acct.ts | 2 +- packages/pl-fe/src/hooks/use-own-account.ts | 2 +- packages/pl-fe/src/init/pl-fe-load.tsx | 2 +- packages/pl-fe/src/layouts/group-layout.tsx | 2 +- packages/pl-fe/src/layouts/home-layout.tsx | 2 +- packages/pl-fe/src/layouts/profile-layout.tsx | 4 +- .../src/layouts/remote-instance-layout.tsx | 2 +- .../pl-fe/src/modals/block-mute-modal.tsx | 4 +- .../src/modals/familiar-followers-modal.tsx | 4 +- .../pl-fe/src/modals/reply-mentions-modal.tsx | 2 +- .../pl-fe/src/modals/report-modal/index.tsx | 4 +- .../pl-fe/src/modals/unauthorized-modal.tsx | 14 +-- .../src/pages/account-lists/directory.tsx | 4 +- .../pages/account-lists/follow-requests.tsx | 4 +- .../src/pages/account-lists/followers.tsx | 4 +- .../src/pages/account-lists/following.tsx | 4 +- .../src/pages/account-lists/subscribers.tsx | 4 +- .../src/pages/accounts/account-gallery.tsx | 12 +-- .../src/pages/accounts/account-timeline.tsx | 20 +--- .../pl-fe/src/pages/dashboard/account.tsx | 26 +++-- .../pl-fe/src/pages/dashboard/dashboard.tsx | 2 +- packages/pl-fe/src/pages/dashboard/report.tsx | 6 +- .../pl-fe/src/pages/dashboard/reports.tsx | 6 +- .../pl-fe/src/pages/developers/create-app.tsx | 2 +- packages/pl-fe/src/pages/fun/circle.tsx | 2 +- .../pages/groups/group-blocked-members.tsx | 4 +- .../groups/group-membership-requests.tsx | 4 +- packages/pl-fe/src/pages/search/search.tsx | 4 +- packages/pl-fe/src/pages/settings/aliases.tsx | 4 +- .../pl-fe/src/pages/settings/edit-profile.tsx | 2 +- .../pl-fe/src/pages/settings/settings.tsx | 2 +- .../status-lists/favourited-statuses.tsx | 6 +- .../status-lists/interaction-requests.tsx | 6 +- .../pages/status-lists/pinned-statuses.tsx | 2 +- .../src/pages/timelines/group-timeline.tsx | 2 +- .../pl-fe/src/queries/accounts/selectors.ts | 26 +++++ .../queries/accounts/use-account-lookup.ts | 58 +++++++++++ .../pl-fe/src/queries/accounts/use-account.ts | 88 +++++++++++++++++ .../src/queries/accounts/use-accounts.ts | 36 +++++++ .../accounts/use-logged-in-accounts.ts | 35 +++++++ .../src/queries/accounts/use-relationship.ts | 27 +++++- .../pl-fe/src/queries/admin/use-accounts.ts | 6 +- .../pl-fe/src/queries/admin/use-reports.ts | 2 +- .../src/queries/admin/use-suggest-account.ts | 47 +++++++++ .../src/queries/admin/use-verify-account.ts | 69 +++++++++++++ packages/pl-fe/src/queries/chats.ts | 2 +- .../src/queries/settings/domain-blocks.ts | 13 ++- .../queries/statuses/use-draft-statuses.ts | 6 +- packages/pl-fe/src/queries/suggestions.ts | 11 ++- .../utils/make-paginated-response-query.ts | 2 +- packages/pl-fe/src/reducers/index.ts | 2 - packages/pl-fe/src/reducers/timelines.ts | 3 +- packages/pl-fe/src/selectors/index.ts | 70 +++---------- packages/pl-fe/src/utils/auth.ts | 4 +- packages/pl-fe/src/utils/state.ts | 2 +- 119 files changed, 659 insertions(+), 1058 deletions(-) delete mode 100644 packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts delete mode 100644 packages/pl-fe/src/api/hooks/accounts/use-account.ts delete mode 100644 packages/pl-fe/src/api/hooks/admin/use-suggest.ts delete mode 100644 packages/pl-fe/src/api/hooks/admin/use-verify.ts delete mode 100644 packages/pl-fe/src/entity-store/actions.ts delete mode 100644 packages/pl-fe/src/entity-store/entities.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/types.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-entity-lookup.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-entity.ts delete mode 100644 packages/pl-fe/src/entity-store/hooks/use-transaction.ts delete mode 100644 packages/pl-fe/src/entity-store/reducer.ts delete mode 100644 packages/pl-fe/src/entity-store/selectors.ts delete mode 100644 packages/pl-fe/src/entity-store/types.ts delete mode 100644 packages/pl-fe/src/entity-store/utils.ts create mode 100644 packages/pl-fe/src/queries/accounts/selectors.ts create mode 100644 packages/pl-fe/src/queries/accounts/use-account-lookup.ts create mode 100644 packages/pl-fe/src/queries/accounts/use-account.ts create mode 100644 packages/pl-fe/src/queries/accounts/use-accounts.ts create mode 100644 packages/pl-fe/src/queries/accounts/use-logged-in-accounts.ts create mode 100644 packages/pl-fe/src/queries/admin/use-suggest-account.ts create mode 100644 packages/pl-fe/src/queries/admin/use-verify-account.ts diff --git a/packages/pl-fe/src/actions/accounts.ts b/packages/pl-fe/src/actions/accounts.ts index 18d567fda..2b19bbcf9 100644 --- a/packages/pl-fe/src/actions/accounts.ts +++ b/packages/pl-fe/src/actions/accounts.ts @@ -1,16 +1,8 @@ -import { type CreateAccountParams, type Relationship } from 'pl-api'; - -import { batcher } from '@/api/batcher'; -import { queryClient } from '@/queries/client'; -import { selectAccount } from '@/selectors'; -import { isLoggedIn } from '@/utils/auth'; - import { getClient } from '../api'; -import { importEntities } from './importer'; - import type { MinifiedStatus } from '@/reducers/statuses'; import type { AppDispatch, RootState } from '@/store'; +import type { CreateAccountParams, Relationship } from 'pl-api'; const ACCOUNT_BLOCK_SUCCESS = 'ACCOUNT_BLOCK_SUCCESS' as const; const ACCOUNT_MUTE_SUCCESS = 'ACCOUNT_MUTE_SUCCESS' as const; @@ -21,95 +13,10 @@ const createAccount = .settings.createAccount(params) .then((response) => ({ params, response })); -const fetchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(fetchRelationships([accountId])); - - const account = selectAccount(getState(), accountId); - - if (account) { - return Promise.resolve(null); - } - - return getClient(getState()) - .accounts.getAccount(accountId) - .then((response) => { - dispatch(importEntities({ accounts: [response] })); - }) - .catch((error) => {}); -}; - -const fetchAccountByUsername = - (username: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const { auth, me } = getState(); - const features = auth.client.features; - - if (features.accountByUsername && (me || !features.accountLookup)) { - return getClient(getState()) - .accounts.getAccount(username) - .then((response) => { - dispatch(fetchRelationships([response.id])); - dispatch(importEntities({ accounts: [response] })); - }); - } else if (features.accountLookup) { - return dispatch(accountLookup(username)).then((account) => { - dispatch(fetchRelationships([account.id])); - }); - } else { - return getClient(getState()) - .accounts.searchAccounts(username, { resolve: true, limit: 1 }) - .then((accounts) => { - const found = accounts.find((a) => a.acct === username); - - if (found) { - dispatch(fetchRelationships([found.id])); - } else { - throw accounts; - } - }); - } - }; - -const fetchRelationships = - (accountIds: string[]) => (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return null; - - const newAccountIds = accountIds.filter( - (id) => !queryClient.getQueryData(['accountRelationships', id]), - ); - - if (newAccountIds.length === 0) { - return null; - } - - const fetcher = batcher.relationships(getClient(getState())).fetch; - - return Promise.all(newAccountIds.map(fetcher)).then((response) => { - dispatch(importEntities({ relationships: response })); - }); - }; - -const accountLookup = - (acct: string, signal?: AbortSignal) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState()) - .accounts.lookupAccount(acct, { signal }) - .then((account) => { - if (account && account.id) dispatch(importEntities({ accounts: [account] })); - return account; - }); - type AccountsAction = { type: typeof ACCOUNT_BLOCK_SUCCESS | typeof ACCOUNT_MUTE_SUCCESS; relationship: Relationship; statuses: Record; }; -export { - ACCOUNT_BLOCK_SUCCESS, - ACCOUNT_MUTE_SUCCESS, - createAccount, - fetchAccount, - fetchAccountByUsername, - fetchRelationships, - accountLookup, - type AccountsAction, -}; +export { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, createAccount, type AccountsAction }; diff --git a/packages/pl-fe/src/actions/auth.ts b/packages/pl-fe/src/actions/auth.ts index 174347d24..a4d07537e 100644 --- a/packages/pl-fe/src/actions/auth.ts +++ b/packages/pl-fe/src/actions/auth.ts @@ -23,8 +23,8 @@ import { createApp } from '@/actions/apps'; import { fetchMeSuccess, fetchMeFail } from '@/actions/me'; import { obtainOAuthToken, revokeOAuthToken } from '@/actions/oauth'; import * as BuildConfig from '@/build-config'; +import { selectAccount } from '@/queries/accounts/selectors'; import { queryClient } from '@/queries/client'; -import { selectAccount } from '@/selectors'; import { unsetSentryAccount } from '@/sentry'; import KVStore from '@/storage/kv-store'; import toast from '@/toast'; @@ -290,8 +290,8 @@ interface SwitchAccountAction { account: Account; } -const switchAccount = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const account = selectAccount(getState(), accountId); +const switchAccount = (accountId: string) => (dispatch: AppDispatch) => { + const account = selectAccount(accountId); if (!account) return; // Clear all stored cache from React Query @@ -304,7 +304,7 @@ const switchAccount = (accountId: string) => (dispatch: AppDispatch, getState: ( const fetchOwnAccounts = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); Object.values(state.auth.users).forEach((user) => { - const account = selectAccount(state, user.id); + const account = selectAccount(user.id); if (!account) { dispatch(verifyCredentials(user.access_token, user.url)).catch(() => { console.warn(`Failed to load account: ${user.url}`); diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index 7d925a31e..c1c64057d 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -5,9 +5,9 @@ import { getClient } from '@/api'; import { isNativeEmoji } from '@/features/emoji'; import emojiSearch from '@/features/emoji/search'; import { Language } from '@/features/preferences'; +import { selectAccount, selectOwnAccount } from '@/queries/accounts/selectors'; import { queryClient } from '@/queries/client'; import { cancelDraftStatus } from '@/queries/statuses/use-draft-statuses'; -import { selectAccount, selectOwnAccount } from '@/selectors'; import { useModalsStore } from '@/stores/modals'; import { useSettingsStore } from '@/stores/settings'; import toast from '@/toast'; @@ -872,7 +872,7 @@ const selectComposeSuggestion = completion = suggestion; startPosition = position - 1; } else if (typeof suggestion === 'string') { - completion = selectAccount(getState(), suggestion)!.acct; + completion = selectAccount(suggestion)!.acct; startPosition = position; } @@ -1006,18 +1006,16 @@ interface ComposeAddToMentionsAction { account: string; } -const addToMentions = - (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const account = selectAccount(state, accountId); - if (!account) return; +const addToMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch) => { + const account = selectAccount(accountId); + if (!account) return; - return dispatch({ - type: COMPOSE_ADD_TO_MENTIONS, - composeId, - account: account.acct, - }); - }; + return dispatch({ + type: COMPOSE_ADD_TO_MENTIONS, + composeId, + account: account.acct, + }); +}; interface ComposeRemoveFromMentionsAction { type: typeof COMPOSE_REMOVE_FROM_MENTIONS; @@ -1025,18 +1023,16 @@ interface ComposeRemoveFromMentionsAction { account: string; } -const removeFromMentions = - (composeId: string, accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const account = selectAccount(state, accountId); - if (!account) return; +const removeFromMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch) => { + const account = selectAccount(accountId); + if (!account) return; - return dispatch({ - type: COMPOSE_REMOVE_FROM_MENTIONS, - composeId, - account: account.acct, - }); - }; + return dispatch({ + type: COMPOSE_REMOVE_FROM_MENTIONS, + composeId, + account: account.acct, + }); +}; interface ComposeEventReplyAction { type: typeof COMPOSE_EVENT_REPLY; diff --git a/packages/pl-fe/src/actions/importer.ts b/packages/pl-fe/src/actions/importer.ts index c26342833..09946541f 100644 --- a/packages/pl-fe/src/actions/importer.ts +++ b/packages/pl-fe/src/actions/importer.ts @@ -1,10 +1,8 @@ -import { importEntities as importEntityStoreEntities } from '@/entity-store/actions'; -import { Entities } from '@/entity-store/entities'; +import { selectAccount } from '@/queries/accounts/selectors'; import { queryClient } from '@/queries/client'; -import { selectAccount } from '@/selectors'; import { useContextStore } from '@/stores/contexts'; -import type { AppDispatch, RootState } from '@/store'; +import type { AppDispatch } from '@/store'; import type { Account as BaseAccount, Group as BaseGroup, @@ -48,11 +46,9 @@ const importEntities = withParents: true, }, ) => - (dispatch: AppDispatch, getState: () => RootState) => { + (dispatch: AppDispatch) => { const override = options.override ?? true; - const state: RootState = !override ? getState() : (undefined as any); - const accounts: Record = {}; const groups: Record = {}; const polls: Record = {}; @@ -60,7 +56,7 @@ const importEntities = const statuses: Record = {}; const processAccount = (account: BaseAccount, withSelf = true) => { - if (!override && selectAccount(state, account.id)) return; + if (!override && selectAccount(account.id)) return; if (withSelf) accounts[account.id] = account; @@ -109,8 +105,11 @@ const importEntities = entities.statuses?.forEach((status) => status && processStatus(status, options.withParents)); } - if (!isEmpty(accounts)) - dispatch(importEntityStoreEntities(Object.values(accounts), Entities.ACCOUNTS)); + if (!isEmpty(accounts)) { + for (const account of Object.values(accounts)) { + queryClient.setQueryData(['accounts', account.id], account); + } + } if (!isEmpty(groups)) for (const group of Object.values(groups)) { queryClient.setQueryData(['groups', group.id], group); diff --git a/packages/pl-fe/src/actions/me.ts b/packages/pl-fe/src/actions/me.ts index d5799596c..ee58776f0 100644 --- a/packages/pl-fe/src/actions/me.ts +++ b/packages/pl-fe/src/actions/me.ts @@ -1,4 +1,4 @@ -import { selectAccount } from '@/selectors'; +import { selectAccount } from '@/queries/accounts/selectors'; import { setSentryAccount } from '@/sentry'; import KVStore from '@/storage/kv-store'; import { useSettingsStore } from '@/stores/settings'; @@ -29,7 +29,7 @@ const getMeId = (state: RootState) => state.me ?? getAuthUserId(state); const getMeUrl = (state: RootState) => { const accountId = getMeId(state); if (accountId) { - return selectAccount(state, accountId)?.url ?? getAuthUserUrl(state); + return selectAccount(accountId)?.url ?? getAuthUserUrl(state); } }; diff --git a/packages/pl-fe/src/actions/moderation.tsx b/packages/pl-fe/src/actions/moderation.tsx index 0bc94d565..570c02074 100644 --- a/packages/pl-fe/src/actions/moderation.tsx +++ b/packages/pl-fe/src/actions/moderation.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { defineMessages, FormattedMessage, IntlShape } from 'react-intl'; -import { fetchAccountByUsername } from '@/actions/accounts'; import { deactivateUser, deleteUser, deleteStatus, toggleStatusSensitivity } from '@/actions/admin'; import OutlineBox from '@/components/outline-box'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import AccountContainer from '@/containers/account-container'; -import { selectAccount } from '@/selectors'; +import { selectAccount } from '@/queries/accounts/selectors'; +import { queryClient } from '@/queries/client'; import { useModalsStore } from '@/stores/modals'; import toast from '@/toast'; @@ -96,10 +96,9 @@ const messages = defineMessages({ const deactivateUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const acct = selectAccount(state, accountId)!.acct; - const name = selectAccount(state, accountId)!.username; + (dispatch: AppDispatch) => { + const acct = selectAccount(accountId)!.acct; + const name = selectAccount(accountId)!.username; const message = ( @@ -135,9 +134,8 @@ const deactivateUserModal = const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const account = selectAccount(state, accountId)!; + (dispatch: AppDispatch) => { + const account = selectAccount(accountId)!; const acct = account.acct; const name = account.username; const local = account.local; @@ -170,7 +168,10 @@ const deleteUserModal = dispatch(deleteUser(accountId)) .then(() => { const message = intl.formatMessage(messages.userDeleted, { acct }); - dispatch(fetchAccountByUsername(acct)); + queryClient.invalidateQueries({ queryKey: ['accounts', accountId] }); + queryClient.invalidateQueries({ + queryKey: ['accounts', 'lookup', acct.toLocaleLowerCase()], + }); toast.success(message); afterConfirm(); }) diff --git a/packages/pl-fe/src/actions/settings.ts b/packages/pl-fe/src/actions/settings.ts index d52229a4f..3f8f932cb 100644 --- a/packages/pl-fe/src/actions/settings.ts +++ b/packages/pl-fe/src/actions/settings.ts @@ -4,7 +4,7 @@ import { patchMe } from '@/actions/me'; import { getClient } from '@/api'; import { NODE_ENV } from '@/build-config'; import messages from '@/messages'; -import { selectOwnAccount } from '@/selectors'; +import { selectOwnAccount } from '@/queries/accounts/selectors'; import KVStore from '@/storage/kv-store'; import { useSettingsStore } from '@/stores/settings'; import toast from '@/toast'; diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts b/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts deleted file mode 100644 index da5bbe6f1..000000000 --- a/packages/pl-fe/src/api/hooks/accounts/use-account-lookup.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { useMemo } from 'react'; - -import { Entities } from '@/entity-store/entities'; -import { useEntityLookup } from '@/entity-store/hooks/use-entity-lookup'; -import { useClient } from '@/hooks/use-client'; -import { useFeatures } from '@/hooks/use-features'; -import { useLoggedIn } from '@/hooks/use-logged-in'; -import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; - -import type { Account } from 'pl-api'; - -interface UseAccountLookupOpts { - withRelationship?: boolean; -} - -const useAccountLookup = (acct: string | undefined, opts: UseAccountLookupOpts = {}) => { - const client = useClient(); - const features = useFeatures(); - const { me } = useLoggedIn(); - const { withRelationship } = opts; - - const { entity, isUnauthorized, ...result } = useEntityLookup( - Entities.ACCOUNTS, - (account) => account.acct.toLowerCase() === acct?.toLowerCase(), - () => client.accounts.lookupAccount(acct!), - { enabled: !!acct }, - ); - - const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( - withRelationship ? entity?.id : undefined, - ); - - const isBlocked = entity?.relationship?.blocked_by === true; - const isUnavailable = me === entity?.id ? false : isBlocked && !features.blockersVisible; - - const account = useMemo( - () => (entity ? { ...entity, relationship } : undefined), - [entity, relationship], - ); - - return { - ...result, - isLoading: result.isLoading, - isRelationshipLoading, - isUnauthorized, - isUnavailable, - account, - }; -}; - -export { useAccountLookup }; diff --git a/packages/pl-fe/src/api/hooks/accounts/use-account.ts b/packages/pl-fe/src/api/hooks/accounts/use-account.ts deleted file mode 100644 index 2a5a75199..000000000 --- a/packages/pl-fe/src/api/hooks/accounts/use-account.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useMemo } from 'react'; - -import { Entities } from '@/entity-store/entities'; -import { useEntity } from '@/entity-store/hooks/use-entity'; -import { useClient } from '@/hooks/use-client'; -import { useFeatures } from '@/hooks/use-features'; -import { useLoggedIn } from '@/hooks/use-logged-in'; -import { useCredentialAccount } from '@/queries/accounts/use-account-credentials'; -import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; - -import type { Account } from 'pl-api'; - -interface UseAccountOpts { - withRelationship?: boolean; -} - -const ADMIN_PERMISSION = 0x1n; - -const hasAdminPermission = (permissions?: string): boolean | undefined => { - if (!permissions) return undefined; - - try { - return (BigInt(permissions) & ADMIN_PERMISSION) === ADMIN_PERMISSION; - } catch { - return undefined; - } -}; - -const useAccount = (accountId?: string, opts: UseAccountOpts = {}) => { - const client = useClient(); - const features = useFeatures(); - const { me } = useLoggedIn(); - const { withRelationship } = opts; - - const { entity, isUnauthorized, ...result } = useEntity( - [Entities.ACCOUNTS, accountId!], - () => client.accounts.getAccount(accountId!), - { enabled: !!accountId }, - ); - - const { data: credentialAccount } = useCredentialAccount(me === accountId); - - const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( - withRelationship ? entity?.id : undefined, - ); - - const isBlocked = entity?.relationship?.blocked_by === true; - const isUnavailable = me === entity?.id ? false : isBlocked && !features.blockersVisible; - - const credentialIsAdmin = useMemo( - () => hasAdminPermission(credentialAccount?.role?.permissions), - [credentialAccount?.role?.permissions], - ); - - const account = useMemo(() => { - if (!entity) return undefined; - - const mergedRelationship = relationship ?? entity.relationship; - const mergedIsAdmin = credentialIsAdmin ?? entity.is_admin; - - if (mergedRelationship === entity.relationship && mergedIsAdmin === entity.is_admin) { - return entity; - } - - return { - ...entity, - relationship: mergedRelationship, - is_admin: mergedIsAdmin, - }; - }, [entity, relationship, credentialIsAdmin]); - - return { - ...result, - isRelationshipLoading, - isUnauthorized, - isUnavailable, - account, - }; -}; - -export { useAccount }; diff --git a/packages/pl-fe/src/api/hooks/admin/use-suggest.ts b/packages/pl-fe/src/api/hooks/admin/use-suggest.ts deleted file mode 100644 index 88ab9564a..000000000 --- a/packages/pl-fe/src/api/hooks/admin/use-suggest.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { EntityCallbacks } from '@/entity-store/hooks/types'; -import { useTransaction } from '@/entity-store/hooks/use-transaction'; -import { useClient } from '@/hooks/use-client'; - -import type { Account } from 'pl-api'; - -const useSuggest = () => { - const client = useClient(); - const { transaction } = useTransaction(); - - const suggestEffect = (accountId: string, suggested: boolean) => { - const updater = (account: Account): Account => { - account.is_suggested = suggested; - return account; - }; - - transaction({ - Accounts: { - [accountId]: updater, - }, - }); - }; - - const suggest = async (accountId: string, callbacks?: EntityCallbacks) => { - suggestEffect(accountId, true); - try { - await client.admin.accounts.suggestUser(accountId); - callbacks?.onSuccess?.(); - } catch (e) { - callbacks?.onError?.(e); - suggestEffect(accountId, false); - } - }; - - const unsuggest = async (accountId: string, callbacks?: EntityCallbacks) => { - suggestEffect(accountId, false); - try { - await client.admin.accounts.unsuggestUser(accountId); - callbacks?.onSuccess?.(); - } catch (e) { - callbacks?.onError?.(e); - suggestEffect(accountId, true); - } - }; - - return { - suggest, - unsuggest, - }; -}; - -export { useSuggest }; diff --git a/packages/pl-fe/src/api/hooks/admin/use-verify.ts b/packages/pl-fe/src/api/hooks/admin/use-verify.ts deleted file mode 100644 index a03b8e24c..000000000 --- a/packages/pl-fe/src/api/hooks/admin/use-verify.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { useTransaction } from '@/entity-store/hooks/use-transaction'; -import { useClient } from '@/hooks/use-client'; - -import type { EntityCallbacks } from '@/entity-store/hooks/types'; -import type { Account } from 'pl-api'; - -const useVerify = () => { - const client = useClient(); - const { transaction } = useTransaction(); - - const verifyEffect = (accountId: string, verified: boolean) => { - const updater = (account: Account): Account => { - if (account.__meta.pleroma) { - const tags = account.__meta.pleroma.tags.filter((tag: string) => tag !== 'verified'); - if (verified) { - tags.push('verified'); - } - account.__meta.pleroma.tags = tags; - } - account.verified = verified; - return account; - }; - - transaction({ - Accounts: { [accountId]: updater }, - }); - }; - - const verify = async (accountId: string, callbacks?: EntityCallbacks) => { - verifyEffect(accountId, true); - try { - await client.admin.accounts.tagUser(accountId, ['verified']); - callbacks?.onSuccess?.(); - } catch (e) { - callbacks?.onError?.(e); - verifyEffect(accountId, false); - } - }; - - const unverify = async (accountId: string, callbacks?: EntityCallbacks) => { - verifyEffect(accountId, false); - try { - await client.admin.accounts.untagUser(accountId, ['verified']); - callbacks?.onSuccess?.(); - } catch (e) { - callbacks?.onError?.(e); - verifyEffect(accountId, true); - } - }; - - return { - verify, - unverify, - }; -}; - -export { useVerify }; diff --git a/packages/pl-fe/src/components/account-hover-card.tsx b/packages/pl-fe/src/components/account-hover-card.tsx index 18ad82a32..ee87432ba 100644 --- a/packages/pl-fe/src/components/account-hover-card.tsx +++ b/packages/pl-fe/src/components/account-hover-card.tsx @@ -4,8 +4,6 @@ import clsx from 'clsx'; import React, { useEffect } from 'react'; import { useIntl, FormattedMessage } from 'react-intl'; -import { fetchRelationships } from '@/actions/accounts'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Badge from '@/components/badge'; import Card, { CardBody } from '@/components/ui/card'; import HStack from '@/components/ui/hstack'; @@ -15,9 +13,9 @@ import Text from '@/components/ui/text'; import ActionButton from '@/features/ui/components/action-button'; import { isTimezoneLabel } from '@/features/ui/components/profile-field'; import { UserPanel } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useAccountScrobbleQuery } from '@/queries/accounts/account-scrobble'; +import { useAccount } from '@/queries/accounts/use-account'; import { useAccountHoverCardActions, useAccountHoverCardStore } from '@/stores/account-hover-card'; import AccountLocalTime from './account-local-time'; @@ -69,7 +67,6 @@ interface IAccountHoverCard { /** Popup profile preview that appears when hovering avatars and display names. */ const AccountHoverCard: React.FC = ({ visible = true }) => { - const dispatch = useAppDispatch(); const router = useRouter(); const intl = useIntl(); @@ -77,14 +74,10 @@ const AccountHoverCard: React.FC = ({ visible = true }) => { const { updateAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions(); const me = useAppSelector((state) => state.me); - const { account } = useAccount(accountId ?? undefined, { withRelationship: true }); + const { data: account } = useAccount(accountId ?? undefined, true); const { data: scrobble } = useAccountScrobbleQuery(account?.id); const badges = getBadges(account); - useEffect(() => { - if (accountId) dispatch(fetchRelationships([accountId])); - }, [dispatch, accountId]); - useEffect(() => { const unlisten = router.subscribe('onLoad', ({ pathChanged }) => { if (pathChanged) { diff --git a/packages/pl-fe/src/components/avatar-stack.tsx b/packages/pl-fe/src/components/avatar-stack.tsx index 693059d48..e37ed52ce 100644 --- a/packages/pl-fe/src/components/avatar-stack.tsx +++ b/packages/pl-fe/src/components/avatar-stack.tsx @@ -3,8 +3,7 @@ import React from 'react'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { selectAccounts } from '@/selectors'; +import { useAccounts } from '@/queries/accounts/use-accounts'; interface IAvatarStack { accountIds: Array; @@ -12,7 +11,7 @@ interface IAvatarStack { } const AvatarStack: React.FC = ({ accountIds, limit = 3 }) => { - const accounts = useAppSelector((state) => selectAccounts(state, accountIds.slice(0, limit))); + const { accounts } = useAccounts(accountIds.slice(0, limit)); return ( diff --git a/packages/pl-fe/src/components/dropdown-navigation.tsx b/packages/pl-fe/src/components/dropdown-navigation.tsx index 0a6172a04..c7a973d3c 100644 --- a/packages/pl-fe/src/components/dropdown-navigation.tsx +++ b/packages/pl-fe/src/components/dropdown-navigation.tsx @@ -2,11 +2,10 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { Link, type LinkOptions } from '@tanstack/react-router'; import clsx from 'clsx'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { fetchOwnAccounts, logOut, switchAccount } from '@/actions/auth'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import Divider from '@/components/ui/divider'; import Icon from '@/components/ui/icon'; @@ -18,11 +17,12 @@ import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; import { useRegistrationStatus } from '@/hooks/use-registration-status'; +import { useAccount } from '@/queries/accounts/use-account'; import { useFollowRequestsCount } from '@/queries/accounts/use-follow-requests'; +import { useLoggedInAccounts } from '@/queries/accounts/use-logged-in-accounts'; import { scheduledStatusesCountQueryOptions } from '@/queries/statuses/scheduled-statuses'; import { useDraftStatusesCountQuery } from '@/queries/statuses/use-draft-statuses'; import { useInteractionRequestsCount } from '@/queries/statuses/use-interaction-requests'; -import { makeGetOtherAccounts } from '@/selectors'; import { useSettings } from '@/stores/settings'; import { useIsSidebarOpen, useUiStoreActions } from '@/stores/ui'; import sourceCode from '@/utils/code'; @@ -83,9 +83,8 @@ const DropdownNavigation: React.FC = React.memo((): React.JSX.Element | null => [me, features], ); - const getOtherAccounts = useCallback(makeGetOtherAccounts(), []); - const { account } = useAccount(me || undefined); - const otherAccounts = useAppSelector((state) => getOtherAccounts(state)); + const { data: account } = useAccount(me || undefined); + const { accounts: otherAccounts } = useLoggedInAccounts(); const settings = useSettings(); const followRequestsCount = useFollowRequestsCount().data ?? 0; const interactionRequestsCount = useInteractionRequestsCount().data ?? 0; diff --git a/packages/pl-fe/src/components/hover-account-wrapper.tsx b/packages/pl-fe/src/components/hover-account-wrapper.tsx index ef118dee0..62aa7fd58 100644 --- a/packages/pl-fe/src/components/hover-account-wrapper.tsx +++ b/packages/pl-fe/src/components/hover-account-wrapper.tsx @@ -2,8 +2,6 @@ import clsx from 'clsx'; import debounce from 'lodash/debounce'; import React, { useRef } from 'react'; -import { fetchAccount } from '@/actions/accounts'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { isMobile } from '@/is-mobile'; import { useAccountHoverCardActions } from '@/stores/account-hover-card'; @@ -21,8 +19,6 @@ interface IHoverAccountWrapper { /** Makes a profile hover card appear when the wrapped element is hovered. */ const HoverAccountWrapper: React.FC = React.memo( ({ accountId, children, element: Elem = 'div', className }) => { - const dispatch = useAppDispatch(); - const { openAccountHoverCard, closeAccountHoverCard } = useAccountHoverCardActions(); const ref = useRef(null); @@ -31,7 +27,6 @@ const HoverAccountWrapper: React.FC = React.memo( if (!accountId) return; if (!isMobile(window.innerWidth)) { - dispatch(fetchAccount(accountId)); showAccountHoverCard(openAccountHoverCard, ref, accountId); } }; diff --git a/packages/pl-fe/src/components/search-input.tsx b/packages/pl-fe/src/components/search-input.tsx index 988842d9d..cfc548257 100644 --- a/packages/pl-fe/src/components/search-input.tsx +++ b/packages/pl-fe/src/components/search-input.tsx @@ -5,8 +5,9 @@ import { defineMessages, useIntl } from 'react-intl'; import AutosuggestAccountInput from '@/components/autosuggest-account-input'; import SvgIcon from '@/components/ui/svg-icon'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { selectAccount } from '@/selectors'; +import { queryClient } from '@/queries/client'; + +import type { Account } from 'pl-api'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -17,7 +18,6 @@ const messages = defineMessages({ const SearchInput = React.memo(() => { const [value, setValue] = useState(''); - const dispatch = useAppDispatch(); const navigate = useNavigate(); const intl = useIntl(); @@ -51,12 +51,13 @@ const SearchInput = React.memo(() => { const handleSelected = (accountId: string) => { setValue(''); - dispatch((_, getState) => + const account = queryClient.getQueryData(['accounts', accountId]); + if (account) { navigate({ to: '/@{$username}', - params: { username: selectAccount(getState(), accountId)!.acct }, - }), - ); + params: { username: account.acct }, + }); + } }; const makeMenu = () => [ diff --git a/packages/pl-fe/src/components/sentry-feedback-form.tsx b/packages/pl-fe/src/components/sentry-feedback-form.tsx index aba6564f9..bd63217cc 100644 --- a/packages/pl-fe/src/components/sentry-feedback-form.tsx +++ b/packages/pl-fe/src/components/sentry-feedback-form.tsx @@ -16,7 +16,7 @@ interface ISentryFeedbackForm { /** Accept feedback for the given Sentry event. */ const SentryFeedbackForm: React.FC = ({ eventId }) => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const [feedback, setFeedback] = useState(); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/packages/pl-fe/src/components/sidebar-navigation.tsx b/packages/pl-fe/src/components/sidebar-navigation.tsx index 4c03f9f46..c5edc0045 100644 --- a/packages/pl-fe/src/components/sidebar-navigation.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation.tsx @@ -67,7 +67,7 @@ const SidebarNavigation: React.FC = React.memo(({ shrink }) const instance = useInstance(); const features = useFeatures(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const { isOpen } = useRegistrationStatus(); const authenticatedScheduledStatusesCountQueryOptions = useMemo( diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 8140a836d..753a71589 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -766,7 +766,7 @@ const MenuButton: React.FC = ({ return autoTranslate && features.translations && renderTranslate && supportsLanguages; }, [me, status, autoTranslate]); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const isStaff = account ? (account.is_admin ?? account.is_moderator) : false; const isAdmin = account ? account.is_admin : false; diff --git a/packages/pl-fe/src/components/status-mention.tsx b/packages/pl-fe/src/components/status-mention.tsx index da1542d86..85fd62587 100644 --- a/packages/pl-fe/src/components/status-mention.tsx +++ b/packages/pl-fe/src/components/status-mention.tsx @@ -1,7 +1,7 @@ import { Link } from '@tanstack/react-router'; import React from 'react'; -import { useAccount } from '@/api/hooks/accounts/use-account'; +import { useAccount } from '@/queries/accounts/use-account'; import HoverAccountWrapper from './hover-account-wrapper'; @@ -11,7 +11,7 @@ interface IStatusMention { } const StatusMention: React.FC = ({ accountId, fallback }) => { - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); if (!account) return ( diff --git a/packages/pl-fe/src/components/statuses/sensitive-content-overlay.tsx b/packages/pl-fe/src/components/statuses/sensitive-content-overlay.tsx index 64520ee5a..6eb1ca14a 100644 --- a/packages/pl-fe/src/components/statuses/sensitive-content-overlay.tsx +++ b/packages/pl-fe/src/components/statuses/sensitive-content-overlay.tsx @@ -73,7 +73,7 @@ const SensitiveContentOverlay = React.forwardRef { const intl = useIntl(); const dispatch = useAppDispatch(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const features = useFeatures(); const queryClient = useQueryClient(); diff --git a/packages/pl-fe/src/components/ui/modal.tsx b/packages/pl-fe/src/components/ui/modal.tsx index ad4830e14..8357d7aa6 100644 --- a/packages/pl-fe/src/components/ui/modal.tsx +++ b/packages/pl-fe/src/components/ui/modal.tsx @@ -14,7 +14,7 @@ const messages = defineMessages({ }); const useDefaultCloseIcon = (): string => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); if ( account?.url === 'https://donotsta.re/users/pmysl' || diff --git a/packages/pl-fe/src/containers/account-container.tsx b/packages/pl-fe/src/containers/account-container.tsx index 9f65ecf2b..3838280dc 100644 --- a/packages/pl-fe/src/containers/account-container.tsx +++ b/packages/pl-fe/src/containers/account-container.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account, { IAccount } from '@/components/account'; +import { useAccount } from '@/queries/accounts/use-account'; interface IAccountContainer extends Omit { id: string; @@ -9,7 +9,7 @@ interface IAccountContainer extends Omit { } const AccountContainer: React.FC = ({ id, withRelationship, ...props }) => { - const { account } = useAccount(id, { withRelationship }); + const { data: account } = useAccount(id, withRelationship); return ; }; diff --git a/packages/pl-fe/src/entity-store/actions.ts b/packages/pl-fe/src/entity-store/actions.ts deleted file mode 100644 index 6d4157b98..000000000 --- a/packages/pl-fe/src/entity-store/actions.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Entities } from './entities'; -import type { EntitiesTransaction, Entity } from './types'; - -const ENTITIES_IMPORT = 'ENTITIES_IMPORT' as const; -const ENTITIES_TRANSACTION = 'ENTITIES_TRANSACTION' as const; - -/** Action to import entities into the cache. */ -const importEntities = (entities: Entity[], entityType: Entities) => ({ - type: ENTITIES_IMPORT, - entityType, - entities, -}); - -const entitiesTransaction = (transaction: EntitiesTransaction) => ({ - type: ENTITIES_TRANSACTION, - transaction, -}); - -/** Any action pertaining to entities. */ -type EntityAction = ReturnType | ReturnType; - -export { - type EntityAction, - ENTITIES_IMPORT, - ENTITIES_TRANSACTION, - importEntities, - entitiesTransaction, -}; diff --git a/packages/pl-fe/src/entity-store/entities.ts b/packages/pl-fe/src/entity-store/entities.ts deleted file mode 100644 index 020d18471..000000000 --- a/packages/pl-fe/src/entity-store/entities.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Account } from 'pl-api'; - -enum Entities { - ACCOUNTS = 'Accounts', -} - -interface EntityTypes { - [Entities.ACCOUNTS]: Account; -} - -export { Entities, type EntityTypes }; diff --git a/packages/pl-fe/src/entity-store/hooks/types.ts b/packages/pl-fe/src/entity-store/hooks/types.ts deleted file mode 100644 index ace66c4b1..000000000 --- a/packages/pl-fe/src/entity-store/hooks/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Entities } from '../entities'; -import type { Entity } from '../types'; -import type { BaseSchema, BaseIssue } from 'valibot'; - -type EntitySchema = BaseSchema>; - -/** Used to look up a single entity by its ID. */ -type EntityPath = [entityType: Entities, entityId: string]; - -/** Callback functions for entity actions. */ -interface EntityCallbacks { - onSuccess?(value: Value): void; - onError?(error: Error): void; -} - -/** - * Passed into hooks to make requests. - * Must return a response. - */ -type EntityFn = (value: T) => Promise; - -export type { EntitySchema, EntityPath, EntityCallbacks, EntityFn }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-entity-lookup.ts b/packages/pl-fe/src/entity-store/hooks/use-entity-lookup.ts deleted file mode 100644 index 2839e18d6..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-entity-lookup.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { useEffect, useState } from 'react'; -import * as v from 'valibot'; - -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { useLoading } from '@/hooks/use-loading'; - -import { importEntities } from '../actions'; -import { findEntity } from '../selectors'; - -import type { Entities } from '../entities'; -import type { Entity } from '../types'; -import type { EntityFn } from './types'; -import type { UseEntityOpts } from './use-entity'; -import type { PlfeResponse } from '@/api'; - -/** Entities will be filtered through this function until it returns true. */ -type LookupFn = (entity: TEntity) => boolean; - -const useEntityLookup = ( - entityType: Entities, - lookupFn: LookupFn, - entityFn: EntityFn, - opts: UseEntityOpts = {}, -) => { - const { schema = v.custom(() => true) } = opts; - - const dispatch = useAppDispatch(); - const [fetchedEntity, setFetchedEntity] = useState(); - const [isFetching, setPromise] = useLoading(true); - const [error, setError] = useState(); - - const entity = useAppSelector( - (state) => findEntity(state, entityType, lookupFn) ?? fetchedEntity, - ); - const isEnabled = opts.enabled ?? true; - const isLoading = isFetching && !entity; - - const fetchEntity = async () => { - try { - const response = await setPromise(entityFn()); - const entity = v.parse(schema, response); - const transformedEntity = opts.transform ? opts.transform(entity) : entity; - setFetchedEntity(transformedEntity as TTransformedEntity); - dispatch(importEntities([transformedEntity], entityType)); - } catch (e) { - setError(e); - } - }; - - useEffect(() => { - if (!isEnabled) return; - - if (!entity || opts.refetch) { - fetchEntity(); - } - }, [isEnabled]); - - return { - entity, - fetchEntity, - isFetching, - isLoading, - isUnauthorized: (error as { response?: PlfeResponse })?.response?.status === 401, - isForbidden: (error as { response?: PlfeResponse })?.response?.status === 403, - }; -}; - -export { useEntityLookup }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-entity.ts b/packages/pl-fe/src/entity-store/hooks/use-entity.ts deleted file mode 100644 index 23c67bc9b..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-entity.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useEffect, useState } from 'react'; -import * as v from 'valibot'; - -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; -import { useLoading } from '@/hooks/use-loading'; - -import { importEntities } from '../actions'; -import { selectEntity } from '../selectors'; - -import type { Entity } from '../types'; -import type { EntitySchema, EntityPath, EntityFn } from './types'; -import type { PlfeResponse } from '@/api'; - -/** Additional options for the hook. */ -interface UseEntityOpts { - /** A valibot schema to parse the API entity. */ - schema?: EntitySchema; - /** Whether to refetch this entity every time the hook mounts, even if it's already in the store. */ - refetch?: boolean; - /** A flag to potentially disable sending requests to the API. */ - enabled?: boolean; - transform?: (schema: TEntity) => TTransformedEntity; -} - -const useEntity = ( - path: EntityPath, - entityFn: EntityFn, - opts: UseEntityOpts = {}, -) => { - const [isFetching, setPromise] = useLoading(true); - const [error, setError] = useState(); - - const dispatch = useAppDispatch(); - - const [entityType, entityId] = path; - - const defaultSchema = v.custom(() => true); - const schema = opts.schema ?? defaultSchema; - - const entity = useAppSelector((state) => - selectEntity(state, entityType, entityId), - ); - - const isEnabled = opts.enabled ?? true; - const isLoading = isFetching && !entity; - const isLoaded = !isFetching && !!entity; - - const fetchEntity = async () => { - try { - const response = await setPromise(entityFn()); - let entity: TEntity | TTransformedEntity = v.parse(schema, response); - if (opts.transform) entity = opts.transform(entity); - dispatch(importEntities([entity], entityType)); - } catch (e) { - setError(e); - } - }; - - useEffect(() => { - if (!isEnabled || error) return; - if (!entity || opts.refetch) { - fetchEntity(); - } - }, [isEnabled]); - - return { - entity, - fetchEntity, - isFetching, - isLoading, - isLoaded, - error, - isUnauthorized: (error as { response?: PlfeResponse })?.response?.status === 401, - isForbidden: (error as { response?: PlfeResponse })?.response?.status === 403, - }; -}; - -export { useEntity, type UseEntityOpts }; diff --git a/packages/pl-fe/src/entity-store/hooks/use-transaction.ts b/packages/pl-fe/src/entity-store/hooks/use-transaction.ts deleted file mode 100644 index baf28069b..000000000 --- a/packages/pl-fe/src/entity-store/hooks/use-transaction.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { entitiesTransaction } from '@/entity-store/actions'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; - -import type { EntityTypes } from '@/entity-store/entities'; -import type { EntitiesTransaction, Entity } from '@/entity-store/types'; - -type Updater = Record TEntity>; - -type Changes = Partial<{ - [K in keyof EntityTypes]: Updater; -}>; - -const useTransaction = () => { - const dispatch = useAppDispatch(); - - const transaction = (changes: Changes): void => { - dispatch(entitiesTransaction(changes as EntitiesTransaction)); - }; - - return { transaction }; -}; - -export { useTransaction }; diff --git a/packages/pl-fe/src/entity-store/reducer.ts b/packages/pl-fe/src/entity-store/reducer.ts deleted file mode 100644 index b24dad8ef..000000000 --- a/packages/pl-fe/src/entity-store/reducer.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { create, type Immutable, type Draft } from 'mutative'; - -import { ENTITIES_IMPORT, ENTITIES_TRANSACTION, type EntityAction } from './actions'; -import { Entities } from './entities'; -import { createCache, updateStore } from './utils'; - -import type { EntitiesTransaction, Entity, EntityCache } from './types'; - -/** Entity reducer state. */ -type State = Immutable<{ - [entityType: string]: EntityCache | undefined; -}>; - -/** Import entities into the cache. */ -const importEntities = (draft: Draft, entityType: Entities, entities: Entity[]) => { - const cache = draft[entityType] ?? createCache(); - cache.store = updateStore(cache.store, entities); - - draft[entityType] = cache; -}; - -const doTransaction = (draft: Draft, transaction: EntitiesTransaction) => { - for (const [entityType, changes] of Object.entries(transaction)) { - const cache = draft[entityType] ?? createCache(); - for (const [id, change] of Object.entries(changes)) { - const entity = cache.store[id]; - if (entity) { - cache.store[id] = change(entity); - } - } - } -}; - -/** Stores various entity data and lists in a one reducer. */ -const reducer = (state: Readonly = {}, action: EntityAction): State => { - switch (action.type) { - case ENTITIES_IMPORT: - return create( - state, - (draft) => { - importEntities(draft, action.entityType, action.entities); - }, - { enableAutoFreeze: true }, - ); - case ENTITIES_TRANSACTION: - return create(state, (draft) => { - doTransaction(draft, action.transaction); - }); - default: - return state; - } -}; - -export { reducer as default }; diff --git a/packages/pl-fe/src/entity-store/selectors.ts b/packages/pl-fe/src/entity-store/selectors.ts deleted file mode 100644 index ce4019e98..000000000 --- a/packages/pl-fe/src/entity-store/selectors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { Entity } from './types'; -import type { RootState } from '@/store'; - -/** Get a single entity by its ID from the store. */ -const selectEntity = ( - state: RootState, - entityType: string, - id: string, -): TEntity | undefined => state.entities[entityType]?.store[id] as TEntity | undefined; - -/** Find an entity using a finder function. */ -const findEntity = ( - state: RootState, - entityType: string, - lookupFn: (entity: TEntity) => boolean, -) => { - const cache = state.entities[entityType]; - - if (cache) { - return (Object.values(cache.store) as TEntity[]).find(lookupFn); - } -}; - -export { selectEntity, findEntity }; diff --git a/packages/pl-fe/src/entity-store/types.ts b/packages/pl-fe/src/entity-store/types.ts deleted file mode 100644 index cbf50cd38..000000000 --- a/packages/pl-fe/src/entity-store/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** A Mastodon API entity. */ -interface Entity { - /** Unique ID for the entity (usually the primary key in the database). */ - id: string; -} - -/** Store of entities by ID. */ -interface EntityStore { - [id: string]: TEntity | undefined; -} - -/** Cache data pertaining to a paritcular entity type. */ -interface EntityCache { - /** Map of entities of this type. */ - store: EntityStore; -} - -/** Whether to import items at the start or end of the list. */ -type ImportPosition = 'start' | 'end'; - -/** Map of entity mutation functions to perform at once on the store. */ -interface EntitiesTransaction { - [entityType: string]: { - [entityId: string]: (entity: TEntity) => TEntity; - }; -} - -export type { Entity, EntityStore, EntityCache, ImportPosition, EntitiesTransaction }; diff --git a/packages/pl-fe/src/entity-store/utils.ts b/packages/pl-fe/src/entity-store/utils.ts deleted file mode 100644 index d919ad280..000000000 --- a/packages/pl-fe/src/entity-store/utils.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Entity, EntityStore, EntityCache } from './types'; - -/** Insert the entities into the store. */ -const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => - entities.reduce( - (store, entity) => { - store[entity.id] = entity; - return store; - }, - { ...store }, - ); - -/** Create an empty entity cache. */ -const createCache = (): EntityCache => ({ - store: {}, -}); - -export { updateStore, createCache }; diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index a69aca6d8..e7beb3b93 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -169,7 +169,7 @@ const Header: React.FC = ({ account }) => { const client = useClient(); const features = useFeatures(); - const { account: ownAccount } = useOwnAccount(); + const { data: ownAccount } = useOwnAccount(); const { mutate: followAccount } = useFollowAccountMutation(account?.id!); const { mutate: unblockAccount } = useUnblockAccountMutation(account?.id!); const { mutate: unmuteAccount } = useUnmuteAccountMutation(account?.id!); diff --git a/packages/pl-fe/src/features/admin/components/report.tsx b/packages/pl-fe/src/features/admin/components/report.tsx index 5d46f8ff0..aa49dd722 100644 --- a/packages/pl-fe/src/features/admin/components/report.tsx +++ b/packages/pl-fe/src/features/admin/components/report.tsx @@ -2,7 +2,6 @@ import { Link } from '@tanstack/react-router'; import React, { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import HoverAccountWrapper from '@/components/hover-account-wrapper'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; @@ -10,6 +9,7 @@ import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useAccount } from '@/queries/accounts/use-account'; import { useReport } from '@/queries/admin/use-reports'; import { makeGetReport } from '@/selectors'; @@ -24,7 +24,7 @@ const Report: React.FC = ({ id }) => { const report = useAppSelector((state) => getReport(state, minifiedReport)); - const { account: targetAccount } = useAccount(report?.target_account_id); + const { data: targetAccount } = useAccount(report?.target_account_id); if (!report) return null; diff --git a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx index 4388c07c2..6425688df 100644 --- a/packages/pl-fe/src/features/auth-login/components/registration-form.tsx +++ b/packages/pl-fe/src/features/auth-login/components/registration-form.tsx @@ -3,7 +3,6 @@ import debounce from 'lodash/debounce'; import React, { useState, useRef, useCallback } from 'react'; import { useIntl, FormattedMessage, defineMessages } from 'react-intl'; -import { accountLookup } from '@/actions/accounts'; import { register, verifyCredentials } from '@/actions/auth'; import BirthdayInput from '@/components/birthday-input'; import Button from '@/components/ui/button'; @@ -16,6 +15,7 @@ import Select from '@/components/ui/select'; import Textarea from '@/components/ui/textarea'; import CaptchaField from '@/features/auth-login/components/captcha'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; import { useModalsActions } from '@/stores/modals'; @@ -52,6 +52,7 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { const navigate = useNavigate(); const dispatch = useAppDispatch(); + const client = useClient(); const { locale } = useSettings(); const features = useFeatures(); const instance = useInstance(); @@ -207,9 +208,10 @@ const RegistrationForm: React.FC = ({ inviteToken }) => { controller.current.abort(); controller.current = new AbortController(); - dispatch( - accountLookup(`${username}${domain ? `@${domain}` : ''}`, controller.current.signal), - ) + client.accounts + .lookupAccount(`${username}${domain ? `@${domain}` : ''}`, { + signal: controller.current.signal, + }) .then((account) => { setUsernameUnavailable(!!account); }) diff --git a/packages/pl-fe/src/features/birthdays/account.tsx b/packages/pl-fe/src/features/birthdays/account.tsx index 93ac3f612..8e20b5062 100644 --- a/packages/pl-fe/src/features/birthdays/account.tsx +++ b/packages/pl-fe/src/features/birthdays/account.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AccountComponent from '@/components/account'; import Icon from '@/components/icon'; import HStack from '@/components/ui/hstack'; +import { useAccount } from '@/queries/accounts/use-account'; const messages = defineMessages({ birthday: { id: 'account.birthday', defaultMessage: 'Born {date}' }, @@ -16,7 +16,7 @@ interface IAccount { const Account: React.FC = ({ accountId }) => { const intl = useIntl(); - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); if (!account) return null; diff --git a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx index d724c92ed..f3773e075 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import { ParsedContent } from '@/components/parsed-content'; import Avatar from '@/components/ui/avatar'; import Emojify from '@/features/emoji/emojify'; import { useFrontendConfig } from '@/hooks/use-frontend-config'; import { useInstance } from '@/hooks/use-instance'; +import { useAccount } from '@/queries/accounts/use-account'; import { useShoutboxMessages } from '@/stores/shoutbox'; import type { Chat } from 'pl-api'; @@ -27,7 +27,7 @@ const ChatListShoutbox: React.FC = ({ onClick }) => }; const lastMessage = messages.at(-1); - const { account: lastMessageAuthor } = useAccount(lastMessage?.author_id); + const { data: lastMessageAuthor } = useAccount(lastMessage?.author_id); return (
{ const { data: accountIds = [], isFetching, hasNextPage, fetchNextPage } = accountSearchResult; - const accounts = useAppSelector((state) => selectAccounts(state, accountIds)); + const { accounts } = useAccounts(accountIds); const [isNearBottom, setNearBottom] = useState(false); const [isNearTop, setNearTop] = useState(true); diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx index 323601774..ad0fd4329 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx @@ -43,7 +43,7 @@ const messages = defineMessages({ }); const ChatsPageSettings = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const intl = useIntl(); const navigate = useNavigate(); const dispatch = useAppDispatch(); diff --git a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx index e702866ee..708ab3d4c 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import React, { useState, useEffect, useRef, useMemo } from 'react'; import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import HoverAccountWrapper from '@/components/hover-account-wrapper'; import { ParsedContent } from '@/components/parsed-content'; import Avatar from '@/components/ui/avatar'; @@ -13,6 +12,7 @@ import Text from '@/components/ui/text'; import Emojify from '@/features/emoji/emojify'; import PlaceholderChatMessage from '@/features/placeholder/components/placeholder-chat-message'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useAccount } from '@/queries/accounts/use-account'; import { useShoutboxIsLoading, useShoutboxMessages, type ShoutMessage } from '@/stores/shoutbox'; import { ChatMessageListList, ChatMessageListScroller } from './chat-message-list'; @@ -25,7 +25,7 @@ interface IShoutboxMessage { } const ShoutboxMessage: React.FC = ({ message, isMyMessage }) => { - const { account } = useAccount(message.author_id); + const { data: account } = useAccount(message.author_id); if (!account) return null; diff --git a/packages/pl-fe/src/features/compose/components/autosuggest-account.tsx b/packages/pl-fe/src/features/compose/components/autosuggest-account.tsx index 10e577dd2..e606eeb55 100644 --- a/packages/pl-fe/src/features/compose/components/autosuggest-account.tsx +++ b/packages/pl-fe/src/features/compose/components/autosuggest-account.tsx @@ -1,14 +1,14 @@ import React from 'react'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; +import { useAccount } from '@/queries/accounts/use-account'; interface IAutosuggestAccount { id: string; } const AutosuggestAccount: React.FC = ({ id }) => { - const { account } = useAccount(id); + const { data: account } = useAccount(id); if (!account) return null; return ( diff --git a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx index ce74123ef..cd638bf32 100644 --- a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx @@ -36,7 +36,7 @@ interface IQuotedStatusContainer { const PreviewComposeContainer: React.FC = ({ composeId }) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const { account: ownAccount } = useOwnAccount(); + const { data: ownAccount } = useOwnAccount(); const previewedStatus = useCompose(composeId).preview as unknown as Status; diff --git a/packages/pl-fe/src/features/compose/containers/warning-container.tsx b/packages/pl-fe/src/features/compose/containers/warning-container.tsx index 1fed10c13..6a66e382e 100644 --- a/packages/pl-fe/src/features/compose/containers/warning-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/warning-container.tsx @@ -2,9 +2,8 @@ import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useCompose } from '@/hooks/use-compose'; -import { selectOwnAccount } from '@/selectors'; +import { useOwnAccount } from '@/hooks/use-own-account'; import Warning from '../components/warning'; @@ -16,12 +15,10 @@ interface IWarningWrapper { const WarningWrapper: React.FC = ({ composeId }) => { const compose = useCompose(composeId); + const { data: account } = useOwnAccount(); - const needsLockWarning = useAppSelector( - (state) => - (compose.visibility === 'private' || compose.visibility === 'mutuals_only') && - !selectOwnAccount(state)!.locked, - ); + const needsLockWarning = + (compose.visibility === 'private' || compose.visibility === 'mutuals_only') && !account?.locked; const hashtagWarning = compose.visibility !== 'public' && compose.visibility !== 'group' && diff --git a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 5de82cb0e..62a795e63 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -37,7 +37,7 @@ import { saveSettings } from '@/actions/settings'; import AutosuggestEmoji from '@/components/autosuggest-emoji'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCompose } from '@/hooks/use-compose'; -import { selectAccount } from '@/selectors'; +import { queryClient } from '@/queries/client'; import { useSettingsStoreActions } from '@/stores/settings'; import { textAtCursorMatchesToken } from '@/utils/suggestions'; @@ -46,6 +46,7 @@ import { $createEmojiNode } from '../nodes/emoji-node'; import { $createMentionNode } from '../nodes/mention-node'; import type { Emoji } from '@/features/emoji'; +import type { Account } from 'pl-api'; type AutoSuggestion = string | Emoji; @@ -319,8 +320,8 @@ const AutosuggestPlugin = ({ (node as TextNode).setTextContent(`${suggestion} `); node.select(); } else { - const account = selectAccount(getState(), suggestion)!; - replaceMatch($createMentionNode(account)); + const account = queryClient.getQueryData(['accounts', suggestion]); + if (account) replaceMatch($createMentionNode(account)); } dispatch(clearComposeSuggestions(composeId)); diff --git a/packages/pl-fe/src/features/conversations/components/conversation.tsx b/packages/pl-fe/src/features/conversations/components/conversation.tsx index 52c1aa240..b0b4c8254 100644 --- a/packages/pl-fe/src/features/conversations/components/conversation.tsx +++ b/packages/pl-fe/src/features/conversations/components/conversation.tsx @@ -1,8 +1,8 @@ import { useNavigate } from '@tanstack/react-router'; import React from 'react'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import StatusContainer from '@/containers/status-container'; +import { useAccount } from '@/queries/accounts/use-account'; import { useMarkConversationRead, type MinifiedConversation, @@ -19,7 +19,7 @@ const Conversation: React.FC = ({ conversation, onMoveUp, onMoveD const { id: conversationId, account_ids, unread, last_status: lastStatusId } = conversation; const { mutate: markConversationRead } = useMarkConversationRead(conversationId); - const { account: lastStatusAccount } = useAccount(account_ids[0]); + const { data: lastStatusAccount } = useAccount(account_ids[0]); const handleClick = () => { if (unread) { diff --git a/packages/pl-fe/src/features/draft-statuses/builder.tsx b/packages/pl-fe/src/features/draft-statuses/builder.tsx index 23cfa9c9d..cd90f4c58 100644 --- a/packages/pl-fe/src/features/draft-statuses/builder.tsx +++ b/packages/pl-fe/src/features/draft-statuses/builder.tsx @@ -1,11 +1,9 @@ -import { statusSchema } from 'pl-api'; +import { statusSchema, type Account } from 'pl-api'; import * as v from 'valibot'; import { normalizeStatus } from '@/normalizers/status'; -import { selectOwnAccount } from '@/selectors'; import type { DraftStatus } from '@/queries/statuses/use-draft-statuses'; -import type { RootState } from '@/store'; const buildPoll = (draftStatus: DraftStatus) => { if (draftStatus.poll?.options) { @@ -19,9 +17,7 @@ const buildPoll = (draftStatus: DraftStatus) => { } }; -const buildStatus = (state: RootState, draftStatus: DraftStatus) => { - const account = selectOwnAccount(state); - +const buildStatus = (account: Account, draftStatus: DraftStatus) => { const status = v.parse(statusSchema, { id: 'draft', account, diff --git a/packages/pl-fe/src/features/draft-statuses/components/draft-status.tsx b/packages/pl-fe/src/features/draft-statuses/components/draft-status.tsx index 3105ec4db..31a752800 100644 --- a/packages/pl-fe/src/features/draft-statuses/components/draft-status.tsx +++ b/packages/pl-fe/src/features/draft-statuses/components/draft-status.tsx @@ -11,7 +11,7 @@ import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import QuotedStatus from '@/features/status/containers/quoted-status-container'; import PollPreview from '@/features/ui/components/poll-preview'; -import { useAppSelector } from '@/hooks/use-app-selector'; +import { useOwnAccount } from '@/hooks/use-own-account'; import { buildStatus } from '../builder'; @@ -24,10 +24,11 @@ interface IDraftStatus { } const DraftStatus: React.FC = ({ draftStatus, ...other }) => { - const status = useAppSelector((state) => { - if (!draftStatus) return null; - return buildStatus(state, draftStatus); - }); + const { data: ownAccount } = useOwnAccount(); + + if (!ownAccount || !draftStatus) return null; + + const status = buildStatus(ownAccount, draftStatus); if (!status) return null; diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index c12f67d8a..0179dca24 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -119,7 +119,7 @@ const EventHeader: React.FC = ({ status }) => { const features = useFeatures(); const { boostModal } = useSettings(); - const { account: ownAccount } = useOwnAccount(); + const { data: ownAccount } = useOwnAccount(); const isStaff = ownAccount ? (ownAccount.is_admin ?? ownAccount.is_moderator) : false; const isAdmin = ownAccount ? ownAccount.is_admin : false; diff --git a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx index 5eb2739ce..82b54ec45 100644 --- a/packages/pl-fe/src/features/group/components/group-member-list-item.tsx +++ b/packages/pl-fe/src/features/group/components/group-member-list-item.tsx @@ -3,11 +3,11 @@ import { GroupRoles } from 'pl-api'; import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import DropdownMenu from '@/components/dropdown-menu/dropdown-menu'; import HStack from '@/components/ui/hstack'; import PlaceholderAccount from '@/features/placeholder/components/placeholder-account'; +import { useAccount } from '@/queries/accounts/use-account'; import { useBlockGroupUserMutation } from '@/queries/groups/use-group-blocks'; import { useDemoteGroupMemberMutation, @@ -78,7 +78,7 @@ const GroupMemberListItem = ({ member, group }: IGroupMemberListItem) => { const { mutate: promoteGroupMember } = usePromoteGroupMemberMutation(group.id); const { mutate: demoteGroupMember } = useDemoteGroupMemberMutation(group.id); - const { account, isLoading } = useAccount(member.account_id); + const { data: account, isLoading } = useAccount(member.account_id); // Current user role const isCurrentUserOwner = group.relationship?.role === GroupRoles.OWNER; diff --git a/packages/pl-fe/src/features/reply-mentions/account.tsx b/packages/pl-fe/src/features/reply-mentions/account.tsx index 7fcac06e0..c5436ddf3 100644 --- a/packages/pl-fe/src/features/reply-mentions/account.tsx +++ b/packages/pl-fe/src/features/reply-mentions/account.tsx @@ -1,13 +1,12 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { fetchAccount } from '@/actions/accounts'; import { addToMentions, removeFromMentions } from '@/actions/compose'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AccountComponent from '@/components/account'; import IconButton from '@/components/ui/icon-button'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCompose } from '@/hooks/use-compose'; +import { useAccount } from '@/queries/accounts/use-account'; const messages = defineMessages({ remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, @@ -25,18 +24,12 @@ const Account: React.FC = ({ composeId, accountId, author }) => { const dispatch = useAppDispatch(); const compose = useCompose(composeId); - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const added = !!account && compose.to?.includes(account.acct); const onRemove = () => dispatch(removeFromMentions(composeId, accountId)); const onAdd = () => dispatch(addToMentions(composeId, accountId)); - useEffect(() => { - if (accountId && !account) { - dispatch(fetchAccount(accountId)); - } - }, []); - if (!account) return null; let button; diff --git a/packages/pl-fe/src/features/scheduled-statuses/builder.tsx b/packages/pl-fe/src/features/scheduled-statuses/builder.tsx index 80b5517b9..c181aff2b 100644 --- a/packages/pl-fe/src/features/scheduled-statuses/builder.tsx +++ b/packages/pl-fe/src/features/scheduled-statuses/builder.tsx @@ -1,14 +1,9 @@ -import { statusSchema, type ScheduledStatus } from 'pl-api'; +import { statusSchema, type Account, type ScheduledStatus } from 'pl-api'; import * as v from 'valibot'; import { normalizeStatus } from '@/normalizers/status'; -import { selectOwnAccount } from '@/selectors'; - -import type { RootState } from '@/store'; - -const buildStatus = (state: RootState, scheduledStatus: ScheduledStatus) => { - const account = selectOwnAccount(state); +const buildStatus = (account: Account, scheduledStatus: ScheduledStatus) => { const poll = scheduledStatus.params.poll ? { id: `${scheduledStatus.id}-poll`, diff --git a/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status.tsx b/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status.tsx index 25ca2008f..abc765687 100644 --- a/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status.tsx +++ b/packages/pl-fe/src/features/scheduled-statuses/components/scheduled-status.tsx @@ -8,7 +8,7 @@ import StatusReplyMentions from '@/components/status-reply-mentions'; import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import PollPreview from '@/features/ui/components/poll-preview'; -import { useAppSelector } from '@/hooks/use-app-selector'; +import { useOwnAccount } from '@/hooks/use-own-account'; import { buildStatus } from '../builder'; @@ -21,9 +21,11 @@ interface IScheduledStatus { } const ScheduledStatus: React.FC = ({ scheduledStatus, ...other }) => { - const status = useAppSelector((state) => { - return buildStatus(state, scheduledStatus); - }); + const { data: ownAccount } = useOwnAccount(); + + if (!ownAccount) return null; + + const status = buildStatus(ownAccount, scheduledStatus); if (!status) return null; diff --git a/packages/pl-fe/src/features/settings/components/messages-settings.tsx b/packages/pl-fe/src/features/settings/components/messages-settings.tsx index 4eced926e..182f590ea 100644 --- a/packages/pl-fe/src/features/settings/components/messages-settings.tsx +++ b/packages/pl-fe/src/features/settings/components/messages-settings.tsx @@ -20,7 +20,7 @@ const messages = defineMessages({ }); const MessagesSettings = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const intl = useIntl(); const updateCredentials = useUpdateCredentials(); diff --git a/packages/pl-fe/src/features/ui/components/panels/instance-moderation-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/instance-moderation-panel.tsx index 95e957ff3..25a1774d2 100644 --- a/packages/pl-fe/src/features/ui/components/panels/instance-moderation-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/instance-moderation-panel.tsx @@ -25,7 +25,7 @@ const InstanceModerationPanel: React.FC = ({ host }) = const intl = useIntl(); const { openModal } = useModalsActions(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const remoteInstance = useAppSelector((state) => getRemoteInstance(state, host)); const handleEditFederation = () => { diff --git a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx index 9e29eb81b..460dced70 100644 --- a/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx +++ b/packages/pl-fe/src/features/ui/components/panels/user-panel.tsx @@ -2,7 +2,6 @@ import { Link } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import StillImage from '@/components/still-image'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; @@ -12,6 +11,7 @@ import Text from '@/components/ui/text'; import VerificationBadge from '@/components/verification-badge'; import Emojify from '@/features/emoji/emojify'; import { useAcct } from '@/hooks/use-acct'; +import { useAccount } from '@/queries/accounts/use-account'; import { useSettings } from '@/stores/settings'; import { shortNumberFormat } from '@/utils/numbers'; @@ -33,7 +33,7 @@ interface IUserPanel { const UserPanel: React.FC = ({ accountId, action, badges, domain }) => { const intl = useIntl(); const { demetricator, disableUserProvidedMedia } = useSettings(); - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const displayedAcct = useAcct(account); if (!account) return null; diff --git a/packages/pl-fe/src/features/ui/components/pending-status.tsx b/packages/pl-fe/src/features/ui/components/pending-status.tsx index f35365550..8b6cd0028 100644 --- a/packages/pl-fe/src/features/ui/components/pending-status.tsx +++ b/packages/pl-fe/src/features/ui/components/pending-status.tsx @@ -11,6 +11,7 @@ import PlaceholderCard from '@/features/placeholder/components/placeholder-card' import PlaceholderMediaGallery from '@/features/placeholder/components/placeholder-media-gallery'; import QuotedStatus from '@/features/status/containers/quoted-status-container'; import { useAppSelector } from '@/hooks/use-app-selector'; +import { useOwnAccount } from '@/hooks/use-own-account'; import { usePendingStatus } from '@/stores/pending-statuses'; import { buildStatus } from '../util/pending-status-builder'; @@ -50,9 +51,12 @@ const PendingStatus: React.FC = ({ variant = 'rounded', }) => { const pendingStatus = usePendingStatus(idempotencyKey); + const { data: ownAccount } = useOwnAccount(); const status = useAppSelector((state) => { - return pendingStatus ? buildStatus(state, pendingStatus, idempotencyKey) : null; + return pendingStatus && ownAccount + ? buildStatus(ownAccount, state, pendingStatus, idempotencyKey) + : null; }); if (!status) return null; diff --git a/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx b/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx index 150491d0d..493781a35 100644 --- a/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-dropdown.tsx @@ -2,16 +2,13 @@ import { Link, type LinkOptions } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { createSelector } from 'reselect'; import { logOut, switchAccount } from '@/actions/auth'; import Account from '@/components/account'; import DropdownMenu from '@/components/dropdown-menu'; -import { Entities } from '@/entity-store/entities'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; -import { RootState } from '@/store'; +import { useLoggedInAccounts } from '@/queries/accounts/use-logged-in-accounts'; import ThemeToggle from './theme-toggle'; @@ -36,23 +33,12 @@ type IMenuItem = { action?: (event: React.MouseEvent) => void; }; -const getOtherAccounts = createSelector( - [ - (state: RootState) => state.auth.users, - (state: RootState) => state.entities[Entities.ACCOUNTS]?.store, - ], - (signedAccounts, accountEntities) => - Object.values(signedAccounts) - .map(({ id }) => accountEntities?.[id] as AccountEntity) - .filter((account) => account), -); - const ProfileDropdown: React.FC = ({ account, children }) => { const dispatch = useAppDispatch(); const features = useFeatures(); const intl = useIntl(); - const otherAccounts = useAppSelector(getOtherAccounts); + const { accounts: otherAccounts } = useLoggedInAccounts(); const handleLogOut = () => { dispatch(logOut()); diff --git a/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx b/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx index 3f0542bb3..cf0e03211 100644 --- a/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx +++ b/packages/pl-fe/src/features/ui/components/profile-familiar-followers.tsx @@ -2,13 +2,13 @@ import { Link } from '@tanstack/react-router'; import React from 'react'; import { FormattedList, FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AvatarStack from '@/components/avatar-stack'; import HoverAccountWrapper from '@/components/hover-account-wrapper'; import HStack from '@/components/ui/hstack'; import Text from '@/components/ui/text'; import VerificationBadge from '@/components/verification-badge'; import Emojify from '@/features/emoji/emojify'; +import { useAccount } from '@/queries/accounts/use-account'; import { useFamiliarFollowers } from '@/queries/accounts/use-familiar-followers'; import { useModalsActions } from '@/stores/modals'; @@ -19,7 +19,7 @@ interface IFamiliarFollowerLink { } const FamiliarFollowerLink: React.FC = ({ id }) => { - const { account } = useAccount(id); + const { data: account } = useAccount(id); if (!account) return null; diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index 4f8d28fed..a8ea33433 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -50,7 +50,7 @@ const UI: React.FC = React.memo(() => { const dispatch = useAppDispatch(); const node = useRef(null); const me = useAppSelector((state) => state.me); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const features = useFeatures(); const vapidKey = useAppSelector((state) => getVapidKey(state)); const client = useClient(); diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index ce81482be..3cf690b58 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -1613,7 +1613,7 @@ const RouterWithContext = () => { const features = useFeatures(); const { cryptoAddresses } = useFrontendConfig(); const hasCrypto = cryptoAddresses.length > 0; - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const context: RouterContext = useMemo( () => ({ diff --git a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx index 6dab900c5..4f01fa91a 100644 --- a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx +++ b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx @@ -46,7 +46,7 @@ const GlobalHotkeys: React.FC = ({ children, node }) => { const navigate = useNavigate(); const { history } = useRouter(); const dispatch = useAppDispatch(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const { openModal } = useModalsActions(); const handlers = useMemo(() => { diff --git a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts index 2fe033d77..7b04bae1a 100644 --- a/packages/pl-fe/src/features/ui/util/pending-status-builder.ts +++ b/packages/pl-fe/src/features/ui/util/pending-status-builder.ts @@ -1,9 +1,8 @@ import { create } from 'mutative'; -import { statusSchema } from 'pl-api'; +import { statusSchema, type Account } from 'pl-api'; import * as v from 'valibot'; import { normalizeStatus } from '@/normalizers/status'; -import { selectOwnAccount } from '@/selectors'; import type { RootState } from '@/store'; import type { PendingStatus } from '@/stores/pending-statuses'; @@ -27,8 +26,12 @@ const buildPoll = (pendingStatus: PendingStatus) => { } }; -const buildStatus = (state: RootState, pendingStatus: PendingStatus, idempotencyKey: string) => { - const account = selectOwnAccount(state)!; +const buildStatus = ( + account: Account, + state: RootState, + pendingStatus: PendingStatus, + idempotencyKey: string, +) => { const inReplyToId = pendingStatus.in_reply_to_id; const status = { diff --git a/packages/pl-fe/src/hooks/use-acct.ts b/packages/pl-fe/src/hooks/use-acct.ts index 9c58897b6..f0a5b7402 100644 --- a/packages/pl-fe/src/hooks/use-acct.ts +++ b/packages/pl-fe/src/hooks/use-acct.ts @@ -11,7 +11,7 @@ import type { Account } from 'pl-api'; const useAcct = (account?: Pick): string | undefined => { const fqn = useAppSelector((state) => displayFqn(state)); const instance = useInstance(); - const localUrl = useOwnAccount().account?.url; + const localUrl = useOwnAccount().data?.url; return useMemo(() => { if (!account) return; diff --git a/packages/pl-fe/src/hooks/use-own-account.ts b/packages/pl-fe/src/hooks/use-own-account.ts index 62059e033..9d0420d93 100644 --- a/packages/pl-fe/src/hooks/use-own-account.ts +++ b/packages/pl-fe/src/hooks/use-own-account.ts @@ -1,4 +1,4 @@ -import { useAccount } from '@/api/hooks/accounts/use-account'; +import { useAccount } from '@/queries/accounts/use-account'; import { useLoggedIn } from './use-logged-in'; diff --git a/packages/pl-fe/src/init/pl-fe-load.tsx b/packages/pl-fe/src/init/pl-fe-load.tsx index c84daa6cc..6ed9ebbe5 100644 --- a/packages/pl-fe/src/init/pl-fe-load.tsx +++ b/packages/pl-fe/src/init/pl-fe-load.tsx @@ -33,7 +33,7 @@ const PlFeLoad: React.FC = ({ children }) => { const dispatch = useAppDispatch(); const me = useAppSelector((state) => state.me); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const locale = useLocale(); const [messages, setMessages] = useState>({}); diff --git a/packages/pl-fe/src/layouts/group-layout.tsx b/packages/pl-fe/src/layouts/group-layout.tsx index 7484128db..1b6bb66e2 100644 --- a/packages/pl-fe/src/layouts/group-layout.tsx +++ b/packages/pl-fe/src/layouts/group-layout.tsx @@ -46,7 +46,7 @@ const GroupLayout = () => { const intl = useIntl(); const location = useLocation(); - const { account: me } = useOwnAccount(); + const { data: me } = useOwnAccount(); const { data: group } = useGroupQuery(groupId, true); const { data: membershipRequests = [] } = useGroupMembershipRequestsQuery(groupId); diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index 78d9bfaae..80d40fe59 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -33,7 +33,7 @@ const HomeLayout = () => { const dispatch = useAppDispatch(); const me = useAppSelector((state) => state.me); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const features = useFeatures(); const frontendConfig = useFrontendConfig(); const { disableUserProvidedMedia } = useSettings(); diff --git a/packages/pl-fe/src/layouts/profile-layout.tsx b/packages/pl-fe/src/layouts/profile-layout.tsx index e174c68fa..933b3bc68 100644 --- a/packages/pl-fe/src/layouts/profile-layout.tsx +++ b/packages/pl-fe/src/layouts/profile-layout.tsx @@ -3,7 +3,6 @@ import React from 'react'; import { Helmet } from 'react-helmet-async'; import { FormattedMessage } from 'react-intl'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import Column from '@/components/ui/column'; import Layout from '@/components/ui/layout'; import Tabs, { type Item } from '@/components/ui/tabs'; @@ -22,13 +21,14 @@ import { import { useAcct } from '@/hooks/use-acct'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; /** Layout to display a user's profile. */ const ProfileLayout: React.FC = () => { const { username } = layouts.profile.useParams(); const location = useLocation(); - const { account, isUnauthorized } = useAccountLookup(username, { withRelationship: true }); + const { data: account, isUnauthorized } = useAccountLookup(username, true); const me = useAppSelector((state) => state.me); const features = useFeatures(); diff --git a/packages/pl-fe/src/layouts/remote-instance-layout.tsx b/packages/pl-fe/src/layouts/remote-instance-layout.tsx index 2f1090e73..44a403bdb 100644 --- a/packages/pl-fe/src/layouts/remote-instance-layout.tsx +++ b/packages/pl-fe/src/layouts/remote-instance-layout.tsx @@ -17,7 +17,7 @@ import { federationRestrictionsDisclosed } from '@/utils/state'; const RemoteInstanceLayout = () => { const { instance } = layouts.remoteInstance.useParams(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const disclosed = useAppSelector(federationRestrictionsDisclosed); return ( diff --git a/packages/pl-fe/src/modals/block-mute-modal.tsx b/packages/pl-fe/src/modals/block-mute-modal.tsx index 3920f1455..3f727952d 100644 --- a/packages/pl-fe/src/modals/block-mute-modal.tsx +++ b/packages/pl-fe/src/modals/block-mute-modal.tsx @@ -2,7 +2,6 @@ import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { initReport, ReportableEntities } from '@/actions/reports'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import FormGroup from '@/components/ui/form-group'; import HStack from '@/components/ui/hstack'; import Modal from '@/components/ui/modal'; @@ -13,6 +12,7 @@ import Toggle from '@/components/ui/toggle'; import DurationSelector from '@/features/compose/components/polls/duration-selector'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; +import { useAccount } from '@/queries/accounts/use-account'; import { useBlockAccountMutation, useMuteAccountMutation, @@ -43,7 +43,7 @@ const BlockMuteModal: React.FC = ({ const dispatch = useAppDispatch(); const intl = useIntl(); - const { account } = useAccount(accountId || undefined, { withRelationship: true }); + const { data: account } = useAccount(accountId || undefined, true); const [notifications, setNotifications] = useState(true); const [duration, setDuration] = useState(0); const [isSubmitting, setIsSubmitting] = useState(false); diff --git a/packages/pl-fe/src/modals/familiar-followers-modal.tsx b/packages/pl-fe/src/modals/familiar-followers-modal.tsx index b11956615..86d9e70a4 100644 --- a/packages/pl-fe/src/modals/familiar-followers-modal.tsx +++ b/packages/pl-fe/src/modals/familiar-followers-modal.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import ScrollableList from '@/components/scrollable-list'; import Modal from '@/components/ui/modal'; import Spinner from '@/components/ui/spinner'; import AccountContainer from '@/containers/account-container'; import Emojify from '@/features/emoji/emojify'; +import { useAccount } from '@/queries/accounts/use-account'; import { useFamiliarFollowers } from '@/queries/accounts/use-familiar-followers'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; @@ -19,7 +19,7 @@ const FamiliarFollowersModal: React.FC { - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const { data: familiarFollowerIds } = useFamiliarFollowers(accountId); const onClickClose = () => { diff --git a/packages/pl-fe/src/modals/reply-mentions-modal.tsx b/packages/pl-fe/src/modals/reply-mentions-modal.tsx index d80037c05..29ec7294b 100644 --- a/packages/pl-fe/src/modals/reply-mentions-modal.tsx +++ b/packages/pl-fe/src/modals/reply-mentions-modal.tsx @@ -23,7 +23,7 @@ const ReplyMentionsModal: React.FC = ( const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector((state) => getStatus(state, { id: compose.inReplyToId! })); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const mentions = statusToMentionsAccountIdsArray(status!, account!, compose.parentRebloggedById); const author = status?.account_id; diff --git a/packages/pl-fe/src/modals/report-modal/index.tsx b/packages/pl-fe/src/modals/report-modal/index.tsx index d3cec29ec..018d161e7 100644 --- a/packages/pl-fe/src/modals/report-modal/index.tsx +++ b/packages/pl-fe/src/modals/report-modal/index.tsx @@ -3,7 +3,6 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { submitReport, ReportableEntities } from '@/actions/reports'; import { fetchAccountTimeline } from '@/actions/timelines'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AttachmentThumbs from '@/components/attachment-thumbs'; import StatusContent from '@/components/status-content'; import Modal from '@/components/ui/modal'; @@ -14,6 +13,7 @@ import AccountContainer from '@/containers/account-container'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; +import { useAccount } from '@/queries/accounts/use-account'; import { useBlockAccountMutation } from '@/queries/accounts/use-relationship'; import ConfirmationStep from './steps/confirmation-step'; @@ -85,7 +85,7 @@ const ReportModal: React.FC = ({ const dispatch = useAppDispatch(); const intl = useIntl(); - const { account } = useAccount(accountId || undefined); + const { data: account } = useAccount(accountId || undefined); const { mutate: blockAccount } = useBlockAccountMutation(accountId); diff --git a/packages/pl-fe/src/modals/unauthorized-modal.tsx b/packages/pl-fe/src/modals/unauthorized-modal.tsx index 049d18109..ec3c777e3 100644 --- a/packages/pl-fe/src/modals/unauthorized-modal.tsx +++ b/packages/pl-fe/src/modals/unauthorized-modal.tsx @@ -8,12 +8,11 @@ import Input from '@/components/ui/input'; import Modal from '@/components/ui/modal'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -import { useAppSelector } from '@/hooks/use-app-selector'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; import { useRegistrationStatus } from '@/hooks/use-registration-status'; -import { selectAccount } from '@/selectors'; +import { useAccount } from '@/queries/accounts/use-account'; import toast from '@/toast'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; @@ -59,14 +58,15 @@ const UnauthorizedModal: React.FC = ({ const instance = useInstance(); const { isOpen } = useRegistrationStatus(); const client = useClient(); + const { data: account } = useAccount(accountId || undefined, false); - const username = useAppSelector((state) => selectAccount(state, accountId!)?.display_name); + const username = account?.display_name; const features = useFeatures(); - const [account, setAccount] = useState(''); + const [remoteAccount, setRemoteAccount] = useState(''); const onAccountChange: React.ChangeEventHandler = (e) => { - setAccount(e.target.value); + setRemoteAccount(e.target.value); }; const onClickClose = () => { @@ -77,7 +77,7 @@ const UnauthorizedModal: React.FC = ({ e.preventDefault(); client.accounts - .remoteInteraction(apId!, account) + .remoteInteraction(apId!, remoteAccount) .then(({ url }) => { window.open(url, '_new', 'noopener,noreferrer'); onClose('UNAUTHORIZED'); @@ -190,7 +190,7 @@ const UnauthorizedModal: React.FC = ({ = ({ id }) => { const me = useAppSelector((state) => state.me); - const { account } = useAccount(id); + const { data: account } = useAccount(id); if (!account) return null; diff --git a/packages/pl-fe/src/pages/account-lists/follow-requests.tsx b/packages/pl-fe/src/pages/account-lists/follow-requests.tsx index b8a11d622..56fbc0635 100644 --- a/packages/pl-fe/src/pages/account-lists/follow-requests.tsx +++ b/packages/pl-fe/src/pages/account-lists/follow-requests.tsx @@ -2,7 +2,6 @@ import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import { AuthorizeRejectButtons } from '@/components/authorize-reject-buttons'; import ScrollableList from '@/components/scrollable-list'; @@ -11,6 +10,7 @@ import Spinner from '@/components/ui/spinner'; import Tabs, { type Item } from '@/components/ui/tabs'; import { followRequestsRoute } from '@/features/ui/router'; import { useFeatures } from '@/hooks/use-features'; +import { useAccount } from '@/queries/accounts/use-account'; import { useAcceptFollowRequestMutation, useFollowRequests, @@ -31,7 +31,7 @@ interface IAccountAuthorize { } const AccountAuthorize: React.FC = ({ id }) => { - const { account } = useAccount(id); + const { data: account } = useAccount(id); const { mutate: authorizeFollowRequest } = useAcceptFollowRequestMutation(id); const { mutate: rejectFollowRequest } = useRejectFollowRequestMutation(id); diff --git a/packages/pl-fe/src/pages/account-lists/followers.tsx b/packages/pl-fe/src/pages/account-lists/followers.tsx index 57c9debc4..d867da405 100644 --- a/packages/pl-fe/src/pages/account-lists/followers.tsx +++ b/packages/pl-fe/src/pages/account-lists/followers.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import MissingIndicator from '@/components/missing-indicator'; import ScrollableList from '@/components/scrollable-list'; import Column from '@/components/ui/column'; @@ -9,6 +8,7 @@ import Spinner from '@/components/ui/spinner'; import AccountContainer from '@/containers/account-container'; import { profileFollowersRoute } from '@/features/ui/router'; import { useFollowers } from '@/queries/account-lists/use-follows'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; const messages = defineMessages({ heading: { id: 'column.followers', defaultMessage: 'Followers' }, @@ -20,7 +20,7 @@ const FollowersPage: React.FC = () => { const intl = useIntl(); - const { account, isUnavailable } = useAccountLookup(username); + const { data: account, isUnavailable } = useAccountLookup(username); const { data = [], diff --git a/packages/pl-fe/src/pages/account-lists/following.tsx b/packages/pl-fe/src/pages/account-lists/following.tsx index bfc4101af..73f02a251 100644 --- a/packages/pl-fe/src/pages/account-lists/following.tsx +++ b/packages/pl-fe/src/pages/account-lists/following.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import MissingIndicator from '@/components/missing-indicator'; import ScrollableList from '@/components/scrollable-list'; import Column from '@/components/ui/column'; @@ -9,6 +8,7 @@ import Spinner from '@/components/ui/spinner'; import AccountContainer from '@/containers/account-container'; import { profileFollowingRoute } from '@/features/ui/router'; import { useFollowing } from '@/queries/account-lists/use-follows'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; const messages = defineMessages({ heading: { id: 'column.following', defaultMessage: 'Following' }, @@ -20,7 +20,7 @@ const FollowingPage: React.FC = () => { const intl = useIntl(); - const { account, isUnavailable } = useAccountLookup(username); + const { data: account, isUnavailable } = useAccountLookup(username); const { data = [], diff --git a/packages/pl-fe/src/pages/account-lists/subscribers.tsx b/packages/pl-fe/src/pages/account-lists/subscribers.tsx index a7f285f7b..aa880af8e 100644 --- a/packages/pl-fe/src/pages/account-lists/subscribers.tsx +++ b/packages/pl-fe/src/pages/account-lists/subscribers.tsx @@ -2,7 +2,6 @@ import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import List, { ListItem } from '@/components/list'; import MissingIndicator from '@/components/missing-indicator'; import ScrollableList from '@/components/scrollable-list'; @@ -12,6 +11,7 @@ import Toggle from '@/components/ui/toggle'; import AccountContainer from '@/containers/account-container'; import { profileSubscribersRoute } from '@/features/ui/router'; import { useSubscribers } from '@/queries/account-lists/use-follows'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; const messages = defineMessages({ heading: { id: 'column.subscribers', defaultMessage: 'Subscribers' }, @@ -25,7 +25,7 @@ const SubscribersPage: React.FC = () => { const intl = useIntl(); - const { account, isUnavailable } = useAccountLookup(username); + const { data: account, isUnavailable } = useAccountLookup(username); const { data = [], diff --git a/packages/pl-fe/src/pages/accounts/account-gallery.tsx b/packages/pl-fe/src/pages/accounts/account-gallery.tsx index 521572606..35545acbd 100644 --- a/packages/pl-fe/src/pages/accounts/account-gallery.tsx +++ b/packages/pl-fe/src/pages/accounts/account-gallery.tsx @@ -3,8 +3,6 @@ import clsx from 'clsx'; import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import Blurhash from '@/components/blurhash'; import Icon from '@/components/icon'; import LoadMore from '@/components/load-more'; @@ -15,6 +13,8 @@ import Spinner from '@/components/ui/spinner'; import { profileMediaRoute } from '@/features/ui/router'; import { type AccountGalleryAttachment, useAccountGallery } from '@/hooks/use-account-gallery'; import { isIOS } from '@/is-mobile'; +import { useAccount } from '@/queries/accounts/use-account'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; @@ -26,7 +26,7 @@ interface IMediaItem { const MediaItem: React.FC = ({ attachment, onOpenMedia, isLast }) => { const { autoPlayGif, displayMedia } = useSettings(); - const { account } = useAccount(attachment.account_id); + const { data: account } = useAccount(attachment.account_id); const [visible, setVisible] = useState( (displayMedia !== 'hide_all' && !attachment.sensitive) || displayMedia === 'show_all', ); @@ -160,11 +160,7 @@ const AccountGalleryPage = () => { const { username } = profileMediaRoute.useParams(); const { openModal } = useModalsActions(); - const { - account, - isLoading: accountLoading, - isUnavailable, - } = useAccountLookup(username, { withRelationship: true }); + const { data: account, isLoading: accountLoading, isUnavailable } = useAccountLookup(username); const { data: attachments, diff --git a/packages/pl-fe/src/pages/accounts/account-timeline.tsx b/packages/pl-fe/src/pages/accounts/account-timeline.tsx index b6abf27db..7573e16ca 100644 --- a/packages/pl-fe/src/pages/accounts/account-timeline.tsx +++ b/packages/pl-fe/src/pages/accounts/account-timeline.tsx @@ -1,9 +1,7 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { fetchAccountByUsername } from '@/actions/accounts'; import { fetchAccountTimeline } from '@/actions/timelines'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import MissingIndicator from '@/components/missing-indicator'; import StatusList from '@/components/status-list'; import Card, { CardBody } from '@/components/ui/card'; @@ -13,6 +11,7 @@ import { profileRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { makeGetStatusIds } from '@/selectors'; import { useSettings } from '@/stores/settings'; @@ -26,8 +25,7 @@ const AccountTimelinePage: React.FC = () => { const features = useFeatures(); const settings = useSettings(); - const { account } = useAccountLookup(username, { withRelationship: true }); - const [accountLoading, setAccountLoading] = useState(!account); + const { data: account, isPending } = useAccountLookup(username); const path = withReplies ? `${account?.id}:with_replies` : account?.id; const showPins = settings.account_timeline.shows.pinned && !withReplies; @@ -47,16 +45,6 @@ const AccountTimelinePage: React.FC = () => { const accountUsername = account?.username ?? username; - useEffect(() => { - dispatch(fetchAccountByUsername(username)) - .then(() => { - setAccountLoading(false); - }) - .catch(() => { - setAccountLoading(false); - }); - }, [username]); - useEffect(() => { if (account) { dispatch(fetchAccountTimeline(account.id, { exclude_replies: !withReplies })); @@ -73,7 +61,7 @@ const AccountTimelinePage: React.FC = () => { } }; - if (!account && accountLoading) { + if (!account && isPending) { return ; } else if (!account) { return ; diff --git a/packages/pl-fe/src/pages/dashboard/account.tsx b/packages/pl-fe/src/pages/dashboard/account.tsx index e78e6d93a..bb22bf411 100644 --- a/packages/pl-fe/src/pages/dashboard/account.tsx +++ b/packages/pl-fe/src/pages/dashboard/account.tsx @@ -4,9 +4,6 @@ import { defineMessages, FormattedMessage, type MessageDescriptor, useIntl } fro import { setBadges as saveBadges, setRole } from '@/actions/admin'; import { deactivateUserModal, deleteUserModal } from '@/actions/moderation'; -import { useAccount } from '@/api/hooks/accounts/use-account'; -import { useSuggest } from '@/api/hooks/admin/use-suggest'; -import { useVerify } from '@/api/hooks/admin/use-verify'; import Account from '@/components/account'; import List, { ListItem } from '@/components/list'; import MissingIndicator from '@/components/missing-indicator'; @@ -24,6 +21,15 @@ import { adminAccountRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useAccount } from '@/queries/accounts/use-account'; +import { + useAdminSuggestAccountMutation, + useAdminUnsuggestAccountMutation, +} from '@/queries/admin/use-suggest-account'; +import { + useAdminVerifyAccountMutation, + useAdminUnverifyAccountMutation, +} from '@/queries/admin/use-verify-account'; import toast from '@/toast'; import { badgeToTag, tagToBadge, getBadges } from '@/utils/badges'; @@ -161,11 +167,13 @@ const AdminAccountPage: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); - const { suggest, unsuggest } = useSuggest(); - const { verify, unverify } = useVerify(); - const { account: ownAccount } = useOwnAccount(); + const { mutate: suggest } = useAdminSuggestAccountMutation(accountId); + const { mutate: unsuggest } = useAdminUnsuggestAccountMutation(accountId); + const { mutate: verify } = useAdminVerifyAccountMutation(accountId); + const { mutate: unverify } = useAdminUnverifyAccountMutation(accountId); + const { data: ownAccount } = useOwnAccount(); const features = useFeatures(); - const { account, isLoading } = useAccount(accountId); + const { data: account, isLoading } = useAccount(accountId); const accountBadges = account ? getBadges(account) : []; const [badges, setBadges] = useState(accountBadges); @@ -192,7 +200,7 @@ const AdminAccountPage: React.FC = () => { const message = checked ? messages.userVerified : messages.userUnverified; const action = checked ? verify : unverify; - action(account.id, { + action(undefined, { onSuccess: () => { toast.success(intl.formatMessage(message, { acct: account.acct })); }, @@ -205,7 +213,7 @@ const AdminAccountPage: React.FC = () => { const message = checked ? messages.userSuggested : messages.userUnsuggested; const action = checked ? suggest : unsuggest; - action(account.id, { + action(undefined, { onSuccess: () => { toast.success(intl.formatMessage(message, { acct: account.acct })); }, diff --git a/packages/pl-fe/src/pages/dashboard/dashboard.tsx b/packages/pl-fe/src/pages/dashboard/dashboard.tsx index d25fdd2f8..460723d35 100644 --- a/packages/pl-fe/src/pages/dashboard/dashboard.tsx +++ b/packages/pl-fe/src/pages/dashboard/dashboard.tsx @@ -25,7 +25,7 @@ const Dashboard: React.FC = () => { const intl = useIntl(); const instance = useInstance(); const features = useFeatures(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const { data: awaitingApprovalCount = 0 } = usePendingUsersCount(); const { data: pendingReportsCount = 0 } = usePendingReportsCount(); diff --git a/packages/pl-fe/src/pages/dashboard/report.tsx b/packages/pl-fe/src/pages/dashboard/report.tsx index 3efd4fb18..9af01ce00 100644 --- a/packages/pl-fe/src/pages/dashboard/report.tsx +++ b/packages/pl-fe/src/pages/dashboard/report.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useState } from 'react'; import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import ReactSwipeableViews from 'react-swipeable-views'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import List, { ListItem } from '@/components/list'; import Card from '@/components/ui/card'; @@ -18,6 +17,7 @@ import ColumnLoading from '@/features/ui/components/column-loading'; import { adminReportRoute } from '@/features/ui/router'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; +import { useAccount } from '@/queries/accounts/use-account'; import { useReopenReport, useReport, @@ -115,8 +115,8 @@ const ReportPage: React.FC = () => { const report = useAppSelector((state) => getReport(state, minifiedReport)); - const { account: authorAccount } = useAccount(report?.account_id); - const { account: targetAccount } = useAccount(report?.target_account_id); + const { data: authorAccount } = useAccount(report?.account_id); + const { data: targetAccount } = useAccount(report?.target_account_id); const { mutate: selfAssignReport } = useSelfAssignReport(reportId); const { mutate: unassignReport } = useUnassignReport(reportId); diff --git a/packages/pl-fe/src/pages/dashboard/reports.tsx b/packages/pl-fe/src/pages/dashboard/reports.tsx index a1994c71c..a7353d87f 100644 --- a/packages/pl-fe/src/pages/dashboard/reports.tsx +++ b/packages/pl-fe/src/pages/dashboard/reports.tsx @@ -2,7 +2,6 @@ import { useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, FormattedList, FormattedMessage, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import ScrollableList from '@/components/scrollable-list'; import Column from '@/components/ui/column'; import HStack from '@/components/ui/hstack'; @@ -10,6 +9,7 @@ import IconButton from '@/components/ui/icon-button'; import Text from '@/components/ui/text'; import Report from '@/features/admin/components/report'; import { adminReportsRoute } from '@/features/ui/router'; +import { useAccount } from '@/queries/accounts/use-account'; import { useReports } from '@/queries/admin/use-reports'; const messages = defineMessages({ @@ -26,8 +26,8 @@ const Reports: React.FC = () => { } = adminReportsRoute.useSearch(); const navigate = useNavigate({ from: adminReportsRoute.fullPath }); - const { account } = useAccount(accountId); - const { account: targetAccount } = useAccount(targetAccountId); + const { data: account } = useAccount(accountId); + const { data: targetAccount } = useAccount(targetAccountId); const { data: reportIds = [], diff --git a/packages/pl-fe/src/pages/developers/create-app.tsx b/packages/pl-fe/src/pages/developers/create-app.tsx index f2f6feb2b..65454687f 100644 --- a/packages/pl-fe/src/pages/developers/create-app.tsx +++ b/packages/pl-fe/src/pages/developers/create-app.tsx @@ -37,7 +37,7 @@ type Params = typeof BLANK_PARAMS; const CreateAppPage: React.FC = () => { const intl = useIntl(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const [app, setApp] = useState | null>(null); const [token, setToken] = useState(null); diff --git a/packages/pl-fe/src/pages/fun/circle.tsx b/packages/pl-fe/src/pages/fun/circle.tsx index db4d430fc..25608cbb6 100644 --- a/packages/pl-fe/src/pages/fun/circle.tsx +++ b/packages/pl-fe/src/pages/fun/circle.tsx @@ -76,7 +76,7 @@ const CirclePage: React.FC = () => { const canvasRef = useRef(null); const { openModal } = useModalsActions(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); useEffect(() => {}, []); diff --git a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx index d3dd35941..f2500e815 100644 --- a/packages/pl-fe/src/pages/groups/group-blocked-members.tsx +++ b/packages/pl-fe/src/pages/groups/group-blocked-members.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import ScrollableList from '@/components/scrollable-list'; import Button from '@/components/ui/button'; @@ -10,6 +9,7 @@ import HStack from '@/components/ui/hstack'; import Spinner from '@/components/ui/spinner'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { groupBlocksRoute } from '@/features/ui/router'; +import { useAccount } from '@/queries/accounts/use-account'; import { useGroupQuery } from '@/queries/groups/use-group'; import { useGroupBlocks, useUnblockGroupUserMutation } from '@/queries/groups/use-group-blocks'; import toast from '@/toast'; @@ -30,7 +30,7 @@ interface IBlockedMember { const BlockedMember: React.FC = ({ accountId, groupId }) => { const intl = useIntl(); - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const { mutate: unblockGroupUser } = useUnblockGroupUserMutation(groupId, accountId); diff --git a/packages/pl-fe/src/pages/groups/group-membership-requests.tsx b/packages/pl-fe/src/pages/groups/group-membership-requests.tsx index 251abe881..c1bd4ac84 100644 --- a/packages/pl-fe/src/pages/groups/group-membership-requests.tsx +++ b/packages/pl-fe/src/pages/groups/group-membership-requests.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import Account from '@/components/account'; import { AuthorizeRejectButtons } from '@/components/authorize-reject-buttons'; import ScrollableList from '@/components/scrollable-list'; @@ -10,6 +9,7 @@ import HStack from '@/components/ui/hstack'; import Spinner from '@/components/ui/spinner'; import ColumnForbidden from '@/features/ui/components/column-forbidden'; import { groupMembershipRequestsRoute } from '@/features/ui/router'; +import { useAccount } from '@/queries/accounts/use-account'; import { useGroupQuery } from '@/queries/groups/use-group'; import { useAcceptGroupMembershipRequestMutation, @@ -37,7 +37,7 @@ interface IMembershipRequest { } const MembershipRequest: React.FC = ({ accountId, onAuthorize, onReject }) => { - const { account } = useAccount(); + const { data: account } = useAccount(accountId); if (!account) return null; diff --git a/packages/pl-fe/src/pages/search/search.tsx b/packages/pl-fe/src/pages/search/search.tsx index 3b39dd621..f16a98454 100644 --- a/packages/pl-fe/src/pages/search/search.tsx +++ b/packages/pl-fe/src/pages/search/search.tsx @@ -4,7 +4,6 @@ import clsx from 'clsx'; import React, { useState } from 'react'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import SearchColumn from '@/columns/search'; import Column from '@/components/ui/column'; import HStack from '@/components/ui/hstack'; @@ -15,6 +14,7 @@ import Tabs from '@/components/ui/tabs'; import Text from '@/components/ui/text'; import { searchRoute } from '@/features/ui/router'; import { useFeatures } from '@/hooks/use-features'; +import { useAccount } from '@/queries/accounts/use-account'; type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links'; @@ -145,7 +145,7 @@ const SearchResults = () => { } else navigate({ search: (prev) => ({ ...prev, type: newActiveFilter }) }); }; - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const handleUnsetAccount = () => { navigate({ search: ({ accountId, ...prev }) => prev }); diff --git a/packages/pl-fe/src/pages/settings/aliases.tsx b/packages/pl-fe/src/pages/settings/aliases.tsx index ec9cd45c2..366d4f869 100644 --- a/packages/pl-fe/src/pages/settings/aliases.tsx +++ b/packages/pl-fe/src/pages/settings/aliases.tsx @@ -2,7 +2,6 @@ import clsx from 'clsx'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AccountComponent from '@/components/account'; import Icon from '@/components/icon'; import ScrollableList from '@/components/scrollable-list'; @@ -14,6 +13,7 @@ import IconButton from '@/components/ui/icon-button'; import Text from '@/components/ui/text'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; +import { useAccount } from '@/queries/accounts/use-account'; import { useSearchAccounts } from '@/queries/search/use-search'; import { useAccountAliases, @@ -40,7 +40,7 @@ const Account: React.FC = ({ accountId, aliases }) => { const features = useFeatures(); const me = useAppSelector((state) => state.me); - const { account } = useAccount(accountId); + const { data: account } = useAccount(accountId); const { mutate: addAccountAlias } = useAddAccountAlias(); diff --git a/packages/pl-fe/src/pages/settings/edit-profile.tsx b/packages/pl-fe/src/pages/settings/edit-profile.tsx index 625364999..7a98dee06 100644 --- a/packages/pl-fe/src/pages/settings/edit-profile.tsx +++ b/packages/pl-fe/src/pages/settings/edit-profile.tsx @@ -277,7 +277,7 @@ const EditProfilePage: React.FC = () => { const instance = useInstance(); const client = useClient(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const features = useFeatures(); const maxFields = instance.configuration.accounts ? instance.configuration.accounts.max_profile_fields diff --git a/packages/pl-fe/src/pages/settings/settings.tsx b/packages/pl-fe/src/pages/settings/settings.tsx index bbf06f77f..56180e6ef 100644 --- a/packages/pl-fe/src/pages/settings/settings.tsx +++ b/packages/pl-fe/src/pages/settings/settings.tsx @@ -49,7 +49,7 @@ const SettingsPage = () => { const { data: mfa } = useMfaConfig(); const features = useFeatures(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const isMfaEnabled = mfa?.settings.totp; diff --git a/packages/pl-fe/src/pages/status-lists/favourited-statuses.tsx b/packages/pl-fe/src/pages/status-lists/favourited-statuses.tsx index f23710f41..d6af7c14d 100644 --- a/packages/pl-fe/src/pages/status-lists/favourited-statuses.tsx +++ b/packages/pl-fe/src/pages/status-lists/favourited-statuses.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccountLookup } from '@/api/hooks/accounts/use-account-lookup'; import MissingIndicator from '@/components/missing-indicator'; import StatusList from '@/components/status-list'; import Column from '@/components/ui/column'; import { profileFavoritesRoute } from '@/features/ui/router'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useAccountLookup } from '@/queries/accounts/use-account-lookup'; import { useFavourites } from '@/queries/status-lists/use-favourites'; const messages = defineMessages({ @@ -18,8 +18,8 @@ const FavouritedStatusesPage: React.FC = () => { const { username } = profileFavoritesRoute.useParams(); const intl = useIntl(); - const { account: ownAccount } = useOwnAccount(); - const { account, isUnavailable } = useAccountLookup(username, { withRelationship: true }); + const { data: ownAccount } = useOwnAccount(); + const { data: account, isUnavailable } = useAccountLookup(username); const isOwnAccount = username.toLowerCase() === ownAccount?.acct?.toLowerCase(); const accountId = isOwnAccount ? undefined : account?.id; diff --git a/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx b/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx index 92a452a0d..34cf4e215 100644 --- a/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx +++ b/packages/pl-fe/src/pages/status-lists/interaction-requests.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import AttachmentThumbs from '@/components/attachment-thumbs'; import Icon from '@/components/icon'; import PullToRefresh from '@/components/pull-to-refresh'; @@ -20,6 +19,7 @@ import { buildLink } from '@/features/notifications/components/notification'; import { Hotkeys } from '@/features/ui/components/hotkeys'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useAccount } from '@/queries/accounts/use-account'; import { type MinifiedInteractionRequest, useAuthorizeInteractionRequestMutation, @@ -122,8 +122,8 @@ const InteractionRequest: React.FC = ({ onMoveDown, }) => { const intl = useIntl(); - const { account: ownAccount } = useOwnAccount(); - const { account } = useAccount(interactionRequest.account_id); + const { data: ownAccount } = useOwnAccount(); + const { data: account } = useAccount(interactionRequest.account_id); const { mutate: authorize } = useAuthorizeInteractionRequestMutation(interactionRequest.id); const { mutate: reject } = useRejectInteractionRequestMutation(interactionRequest.id); diff --git a/packages/pl-fe/src/pages/status-lists/pinned-statuses.tsx b/packages/pl-fe/src/pages/status-lists/pinned-statuses.tsx index 67d497f64..2a708f2bf 100644 --- a/packages/pl-fe/src/pages/status-lists/pinned-statuses.tsx +++ b/packages/pl-fe/src/pages/status-lists/pinned-statuses.tsx @@ -16,7 +16,7 @@ const PinnedStatusesPage = () => { const intl = useIntl(); const { username } = profilePinsRoute.useParams(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const { data: statusIds = [], isFetching: isLoading, diff --git a/packages/pl-fe/src/pages/timelines/group-timeline.tsx b/packages/pl-fe/src/pages/timelines/group-timeline.tsx index fb45dbd65..7cb151890 100644 --- a/packages/pl-fe/src/pages/timelines/group-timeline.tsx +++ b/packages/pl-fe/src/pages/timelines/group-timeline.tsx @@ -25,7 +25,7 @@ const GroupTimelinePage: React.FC = () => { const { groupId } = groupTimelineRoute.useParams(); const intl = useIntl(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const dispatch = useAppDispatch(); const composer = useRef(null); diff --git a/packages/pl-fe/src/queries/accounts/selectors.ts b/packages/pl-fe/src/queries/accounts/selectors.ts new file mode 100644 index 000000000..ca7bdbe82 --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/selectors.ts @@ -0,0 +1,26 @@ +import { queryClient } from '@/queries/client'; + +import type { RootState } from '@/store'; +import type { Account } from 'pl-api'; + +const getAccounts = (): Array => + queryClient + .getQueriesData({ queryKey: ['accounts'] }) + .map(([, account]) => account) + .filter((account): account is Account => !!account && typeof account.id === 'string'); + +const selectAccount = (accountId: string) => + queryClient.getQueryData(['accounts', accountId]); + +const selectAccounts = (accountIds: Array) => + accountIds + .map((accountId) => selectAccount(accountId)) + .filter((account): account is Account => account !== undefined); + +const selectOwnAccount = (state: RootState) => { + if (state.me) { + return selectAccount(state.me); + } +}; + +export { getAccounts, selectAccount, selectAccounts, selectOwnAccount }; diff --git a/packages/pl-fe/src/queries/accounts/use-account-lookup.ts b/packages/pl-fe/src/queries/accounts/use-account-lookup.ts new file mode 100644 index 000000000..90b893d4c --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/use-account-lookup.ts @@ -0,0 +1,58 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; + +import { useClient } from '@/hooks/use-client'; +import { useFeatures } from '@/hooks/use-features'; + +import { useAccount } from './use-account'; + +import type { PlfeResponse } from '@/api'; + +const getResponseStatus = (error: unknown) => + (error as { response?: PlfeResponse })?.response?.status; + +const useAccountLookup = (acct: string | undefined, withRelationship = false) => { + const client = useClient(); + const features = useFeatures(); + const queryClient = useQueryClient(); + + const accountIdQuery = useQuery({ + queryKey: ['accounts', 'lookup', acct?.toLowerCase()], + queryFn: async ({ signal }) => { + let account; + + if (features.accountLookup) { + account = await client.accounts.lookupAccount(acct!, { signal }); + } else if (features.accountByUsername) { + account = await client.accounts.getAccount(acct!); + } else { + const results = await client.accounts.searchAccounts( + acct!, + { resolve: true, limit: 1 }, + { signal }, + ); + account = results.find((result) => result.acct.toLowerCase() === acct!.toLowerCase()); + } + + if (account) { + queryClient.setQueryData(['accounts', account.id], account); + return account.id; + } + }, + enabled: !!acct, + }); + + const accountQuery = useAccount(accountIdQuery.data, withRelationship); + const lookupIsUnauthorized = getResponseStatus(accountIdQuery.error) === 401; + + return { + ...accountQuery, + error: accountIdQuery.error ?? accountQuery.error, + isError: accountIdQuery.isError || accountQuery.isError, + isLoading: accountIdQuery.isLoading || accountQuery.isLoading, + isFetching: accountIdQuery.isFetching || accountQuery.isFetching, + isPending: accountIdQuery.isPending || accountQuery.isPending, + isUnauthorized: lookupIsUnauthorized || accountQuery.isUnauthorized, + }; +}; + +export { useAccountLookup }; diff --git a/packages/pl-fe/src/queries/accounts/use-account.ts b/packages/pl-fe/src/queries/accounts/use-account.ts new file mode 100644 index 000000000..c7c65eb85 --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/use-account.ts @@ -0,0 +1,88 @@ +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { useClient } from '@/hooks/use-client'; +import { useFeatures } from '@/hooks/use-features'; +import { useLoggedIn } from '@/hooks/use-logged-in'; +import { useCredentialAccount } from '@/queries/accounts/use-account-credentials'; +import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; + +import type { PlfeResponse } from '@/api'; + +const ADMIN_PERMISSION = 0x1n; + +const getResponseStatus = (error: unknown) => + (error as { response?: PlfeResponse })?.response?.status; + +const hasAdminPermission = (permissions?: string): boolean | undefined => { + if (!permissions) return undefined; + + try { + return (BigInt(permissions) & ADMIN_PERMISSION) === ADMIN_PERMISSION; + } catch { + return undefined; + } +}; + +const useAccount = (accountId?: string, withRelationship = false) => { + const client = useClient(); + const features = useFeatures(); + const { me } = useLoggedIn(); + const queryClient = useQueryClient(); + + const accountQuery = useQuery({ + queryKey: ['accounts', accountId], + queryFn: async () => { + const account = await client.accounts.getAccount(accountId!); + queryClient.setQueryData(['accounts', 'lookup', account.acct.toLowerCase()], account); + return account; + }, + enabled: !!accountId, + }); + + const { data: credentialAccount } = useCredentialAccount(me === accountId); + + const { data: relationship, isLoading: isRelationshipLoading } = useRelationshipQuery( + withRelationship ? accountQuery.data?.id : undefined, + ); + + const isBlocked = accountQuery.data?.relationship?.blocked_by === true; + const isUnavailable = + me === accountQuery.data?.id ? false : isBlocked && !features.blockersVisible; + const isUnauthorized = getResponseStatus(accountQuery.error) === 401; + + const credentialIsAdmin = useMemo( + () => hasAdminPermission(credentialAccount?.role?.permissions), + [credentialAccount?.role?.permissions], + ); + + const account = useMemo(() => { + if (!accountQuery.data) return undefined; + + const mergedRelationship = relationship ?? accountQuery.data.relationship; + const mergedIsAdmin = credentialIsAdmin ?? accountQuery.data.is_admin; + + if ( + mergedRelationship === accountQuery.data.relationship && + mergedIsAdmin === accountQuery.data.is_admin + ) { + return accountQuery.data; + } + + return { + ...accountQuery.data, + relationship: mergedRelationship, + is_admin: mergedIsAdmin, + }; + }, [accountQuery.data, relationship, credentialIsAdmin]); + + return { + ...accountQuery, + isRelationshipLoading, + isUnauthorized, + isUnavailable, + data: account, + }; +}; + +export { useAccount }; diff --git a/packages/pl-fe/src/queries/accounts/use-accounts.ts b/packages/pl-fe/src/queries/accounts/use-accounts.ts new file mode 100644 index 000000000..c71267a9c --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/use-accounts.ts @@ -0,0 +1,36 @@ +import { useQueries, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { useClient } from '@/hooks/use-client'; + +import type { Account } from 'pl-api'; + +const useAccounts = (accountIds: Array) => { + const client = useClient(); + const queryClient = useQueryClient(); + + const queries = useQueries({ + queries: accountIds.map((accountId) => ({ + queryKey: ['accounts', accountId], + queryFn: async () => { + const response = await client.accounts.getAccount(accountId); + queryClient.setQueryData(['accounts', 'lookup', response.acct.toLowerCase()], response); + return response; + }, + enabled: !!accountId, + })), + }); + + const accounts = useMemo( + () => queries.map((query) => query.data).filter((account): account is Account => !!account), + [queries], + ); + + return { + accounts, + isLoading: queries.some((query) => query.isLoading), + isFetching: queries.some((query) => query.isFetching), + }; +}; + +export { useAccounts }; diff --git a/packages/pl-fe/src/queries/accounts/use-logged-in-accounts.ts b/packages/pl-fe/src/queries/accounts/use-logged-in-accounts.ts new file mode 100644 index 000000000..c45335c4e --- /dev/null +++ b/packages/pl-fe/src/queries/accounts/use-logged-in-accounts.ts @@ -0,0 +1,35 @@ +import { skipToken, useQueries } from '@tanstack/react-query'; +import { useMemo } from 'react'; + +import { useAppSelector } from '@/hooks/use-app-selector'; +import { validId } from '@/utils/auth'; + +import type { Account } from 'pl-api'; + +/** doesn't fetch because it's a hack that should not exist like this */ +const useLoggedInAccounts = () => { + const { me, accountIds } = useAppSelector((state) => ({ + me: state.me, + accountIds: Object.values(state.auth.users) + .map((authUser) => authUser?.id) + .filter((id): id is string => validId(id)), + })); + + const otherAccountIds = useMemo(() => accountIds.filter((id) => id !== me), [accountIds, me]); + + const queries = useQueries({ + queries: otherAccountIds.map((accountId) => ({ + queryKey: ['accounts', accountId] as const, + queryFn: skipToken, + })), + }); + + const accounts = useMemo( + () => queries.map((q) => q.data).filter((account): account is Account => !!account), + [queries], + ); + + return { accounts }; +}; + +export { useLoggedInAccounts }; diff --git a/packages/pl-fe/src/queries/accounts/use-relationship.ts b/packages/pl-fe/src/queries/accounts/use-relationship.ts index 04552782b..f73ccabfd 100644 --- a/packages/pl-fe/src/queries/accounts/use-relationship.ts +++ b/packages/pl-fe/src/queries/accounts/use-relationship.ts @@ -1,4 +1,5 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMemo } from 'react'; import { ACCOUNT_BLOCK_SUCCESS, @@ -64,6 +65,29 @@ const useRelationshipQuery = (accountId?: string) => { }); }; +const useRelationshipsQuery = (accountIds?: Array) => { + const client = useClient(); + const { isLoggedIn } = useLoggedIn(); + + const queries = useMemo( + () => + isLoggedIn && accountIds + ? accountIds.map((accountId) => ({ + queryKey: ['accountRelationships', accountId] as const, + queryFn: () => + batcher + .relationships(client) + .fetch(accountId) + .then((data) => data || undefined), + enabled: !!accountId, + })) + : [], + [isLoggedIn, accountIds?.join(',')], + ); + + return useQueries({ queries }); +}; + const useFollowAccountMutation = (accountId: string) => { const client = useClient(); const queryClient = useQueryClient(); @@ -371,6 +395,7 @@ const useUpdateAccountNoteMutation = (accountId: string) => { export { useRelationshipQuery, + useRelationshipsQuery, useFollowAccountMutation, useUnfollowAccountMutation, useBlockAccountMutation, diff --git a/packages/pl-fe/src/queries/admin/use-accounts.ts b/packages/pl-fe/src/queries/admin/use-accounts.ts index 559166d62..e65a1709f 100644 --- a/packages/pl-fe/src/queries/admin/use-accounts.ts +++ b/packages/pl-fe/src/queries/admin/use-accounts.ts @@ -7,10 +7,10 @@ import { } from '@tanstack/react-query'; import { importEntities } from '@/actions/importer'; -import { useAccount } from '@/api/hooks/accounts/use-account'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useAccount } from '@/queries/accounts/use-account'; import { filterById } from '../utils/filter-id'; import { makePaginatedResponseQuery } from '../utils/make-paginated-response-query'; @@ -51,7 +51,7 @@ const useAdminAccount = (accountId?: string) => { enabled: !!accountId, }); - const { account } = useAccount(query.data ? accountId : undefined); + const { data: account } = useAccount(query.data ? accountId : undefined); if (query.data && account) query.data.account = account; @@ -67,7 +67,7 @@ const pendingUsersQuery = makePaginatedResponseQueryOptions( )(); const usePendingUsersCount = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); return useInfiniteQuery({ ...pendingUsersQuery, diff --git a/packages/pl-fe/src/queries/admin/use-reports.ts b/packages/pl-fe/src/queries/admin/use-reports.ts index c24f91471..85348a1d4 100644 --- a/packages/pl-fe/src/queries/admin/use-reports.ts +++ b/packages/pl-fe/src/queries/admin/use-reports.ts @@ -33,7 +33,7 @@ const pendingReportsQuery = makePaginatedResponseQueryOptions( )(); const usePendingReportsCount = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const instance = useInstance(); return useInfiniteQuery({ diff --git a/packages/pl-fe/src/queries/admin/use-suggest-account.ts b/packages/pl-fe/src/queries/admin/use-suggest-account.ts new file mode 100644 index 000000000..b7e95b81b --- /dev/null +++ b/packages/pl-fe/src/queries/admin/use-suggest-account.ts @@ -0,0 +1,47 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useClient } from '@/hooks/use-client'; + +import type { Account } from 'pl-api'; + +const useAdminSuggestAccountMutation = (accountId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'accounts', accountId, 'suggest'], + mutationFn: () => client.admin.accounts.suggestUser(accountId), + onMutate: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + account ? { ...account, is_suggested: true } : undefined, + ); + }, + onError: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + account ? { ...account, is_suggested: false } : undefined, + ); + }, + }); +}; + +const useAdminUnsuggestAccountMutation = (accountId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'accounts', accountId, 'unsuggest'], + mutationFn: () => client.admin.accounts.unsuggestUser(accountId), + onMutate: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + account ? { ...account, is_suggested: false } : undefined, + ); + }, + onError: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + account ? { ...account, is_suggested: true } : undefined, + ); + }, + }); +}; + +export { useAdminSuggestAccountMutation, useAdminUnsuggestAccountMutation }; diff --git a/packages/pl-fe/src/queries/admin/use-verify-account.ts b/packages/pl-fe/src/queries/admin/use-verify-account.ts new file mode 100644 index 000000000..dd3078d66 --- /dev/null +++ b/packages/pl-fe/src/queries/admin/use-verify-account.ts @@ -0,0 +1,69 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { useClient } from '@/hooks/use-client'; + +import type { Account } from 'pl-api'; + +const setVerified = (account: Account | undefined, verified: boolean): Account | undefined => { + if (!account) return account; + + const existingTags = account.__meta?.pleroma?.tags ?? []; + const tags = existingTags.filter((tag: string) => tag !== 'verified'); + if (verified) tags.push('verified'); + + return { + ...account, + __meta: account.__meta?.pleroma + ? { + ...account.__meta, + pleroma: { + ...account.__meta.pleroma, + tags, + }, + } + : account.__meta, + verified, + }; +}; + +const useAdminVerifyAccountMutation = (accountId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'accounts', accountId, 'verify'], + mutationFn: () => client.admin.accounts.tagUser(accountId, ['verified']), + onMutate: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + setVerified(account, true), + ); + }, + onError: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + setVerified(account, false), + ); + }, + }); +}; + +const useAdminUnverifyAccountMutation = (accountId: string) => { + const client = useClient(); + const queryClient = useQueryClient(); + + return useMutation({ + mutationKey: ['admin', 'accounts', accountId, 'unverify'], + mutationFn: () => client.admin.accounts.untagUser(accountId, ['verified']), + onMutate: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + setVerified(account, false), + ); + }, + onError: () => { + queryClient.setQueryData(['accounts', accountId], (account) => + setVerified(account, true), + ); + }, + }); +}; + +export { useAdminVerifyAccountMutation, useAdminUnverifyAccountMutation }; diff --git a/packages/pl-fe/src/queries/chats.ts b/packages/pl-fe/src/queries/chats.ts index b407f58bf..e2ae2b4e7 100644 --- a/packages/pl-fe/src/queries/chats.ts +++ b/packages/pl-fe/src/queries/chats.ts @@ -165,7 +165,7 @@ const useMarkChatAsRead = (chatId: string) => { }; const useCreateChatMessage = (chatId: string) => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const client = useClient(); const { chat } = useChatContext(); diff --git a/packages/pl-fe/src/queries/settings/domain-blocks.ts b/packages/pl-fe/src/queries/settings/domain-blocks.ts index 6e6cb3480..bc3f439e8 100644 --- a/packages/pl-fe/src/queries/settings/domain-blocks.ts +++ b/packages/pl-fe/src/queries/settings/domain-blocks.ts @@ -1,12 +1,10 @@ import { getClient } from '@/api'; -import { Entities } from '@/entity-store/entities'; import { queryClient } from '../client'; import { makePaginatedResponseQueryOptions } from '../utils/make-paginated-response-query-options'; import { mutationOptions } from '../utils/mutation-options'; import type { MinifiedSuggestion } from '../trends/use-suggested-accounts'; -import type { EntityStore } from '@/entity-store/types'; import type { RootState, Store } from '@/store'; import type { Account } from 'pl-api'; @@ -44,11 +42,12 @@ const unblockDomainMutationOptions = mutationOptions({ }); const selectAccountsByDomain = (state: RootState, domain: string): string[] => { - const store = state.entities[Entities.ACCOUNTS]?.store as EntityStore | undefined; - const entries = store ? Object.entries(store) : undefined; - const accounts = entries - ?.filter(([_, item]) => item && item.acct.endsWith(`@${domain}`)) - .map(([_, item]) => item!.id); + const accounts = queryClient + .getQueriesData({ queryKey: ['accounts'] }) + .map(([, account]) => account) + .filter((account): account is Account => !!account && typeof account.id === 'string') + .filter((account) => account.acct.endsWith(`@${domain}`)) + .map((account) => account.id); return accounts ?? []; }; diff --git a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts index df65be2e5..ce8532daf 100644 --- a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts +++ b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts @@ -54,7 +54,7 @@ const persistDrafts = (accountUrl: string, drafts: Record) => KVStore.setItem(`drafts:${accountUrl}`, Object.values(drafts)); const useDraftStatusesQuery = (select?: (data: Record) => T) => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); return useQuery({ queryKey: ['draftStatuses'], @@ -71,7 +71,7 @@ const useDraftStatusesCountQuery = () => useDraftStatusesQuery((data) => Object.values(data).length); const usePersistDraftStatus = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const dispatch = useAppDispatch(); const queryClient = useQueryClient(); @@ -108,7 +108,7 @@ const cancelDraftStatus = (queryClient: QueryClient, accountUrl: string, draftId }; const useCancelDraftStatus = () => { - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); const queryClient = useQueryClient(); return (draftId: string) => cancelDraftStatus(queryClient, account!.url, draftId); diff --git a/packages/pl-fe/src/queries/suggestions.ts b/packages/pl-fe/src/queries/suggestions.ts index 689fa2e23..ca183fa26 100644 --- a/packages/pl-fe/src/queries/suggestions.ts +++ b/packages/pl-fe/src/queries/suggestions.ts @@ -1,6 +1,5 @@ import { useMutation, keepPreviousData, useQuery } from '@tanstack/react-query'; -import { fetchRelationships } from '@/actions/accounts'; import { importEntities } from '@/actions/importer'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; @@ -8,6 +7,8 @@ import { useLoggedIn } from '@/hooks/use-logged-in'; import { removePageItem } from '../utils/queries'; +import { useRelationshipsQuery } from './accounts/use-relationship'; + const SuggestionKeys = { suggestions: ['suggestions'] as const, }; @@ -21,19 +22,21 @@ const useSuggestions = () => { const response = await client.myAccount.getSuggestions(); const accounts = response.map(({ account }) => account); - const accountIds = accounts.map((account) => account.id); dispatch(importEntities({ accounts })); - dispatch(fetchRelationships(accountIds)); return response.map(({ account, ...x }) => ({ ...x, account_id: account.id })); }; - return useQuery({ + const query = useQuery({ queryKey: SuggestionKeys.suggestions, queryFn: () => getSuggestions(), placeholderData: keepPreviousData, enabled: isLoggedIn, }); + + useRelationshipsQuery(query.data?.map((s) => s.account_id)); + + return query; }; const useDismissSuggestion = () => { diff --git a/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts b/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts index 4bc1c3306..61e5bebbe 100644 --- a/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts +++ b/packages/pl-fe/src/queries/utils/make-paginated-response-query.ts @@ -30,7 +30,7 @@ const makePaginatedResponseQuery = ) => (...params: T1) => { const client = useClient(); - const { account } = useOwnAccount(); + const { data: account } = useOwnAccount(); return useInfiniteQuery({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index d44c4423b..8779c276c 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -2,7 +2,6 @@ import { combineReducers } from '@reduxjs/toolkit'; import { AUTH_LOGGED_OUT } from '@/actions/auth'; import * as BuildConfig from '@/build-config'; -import entities from '@/entity-store/reducer'; import admin from './admin'; import auth from './auth'; @@ -20,7 +19,6 @@ const reducers = { admin, auth, compose, - entities, filters, frontendConfig, instance, diff --git a/packages/pl-fe/src/reducers/timelines.ts b/packages/pl-fe/src/reducers/timelines.ts index 9449eb0a0..cc4e4cce5 100644 --- a/packages/pl-fe/src/reducers/timelines.ts +++ b/packages/pl-fe/src/reducers/timelines.ts @@ -25,7 +25,6 @@ import { type TimelineAction, } from '../actions/timelines'; -import type { ImportPosition } from '@/entity-store/types'; import type { Status } from '@/normalizers/status'; import type { PaginatedResponse, @@ -34,6 +33,8 @@ import type { CreateStatusParams, } from 'pl-api'; +type ImportPosition = 'start' | 'end'; + const TRUNCATE_LIMIT = 40; const TRUNCATE_SIZE = 20; diff --git a/packages/pl-fe/src/selectors/index.ts b/packages/pl-fe/src/selectors/index.ts index b9cb5695a..d9fdff097 100644 --- a/packages/pl-fe/src/selectors/index.ts +++ b/packages/pl-fe/src/selectors/index.ts @@ -1,33 +1,17 @@ import { createSelector } from 'reselect'; -import { Entities } from '@/entity-store/entities'; +import { getAccounts, selectAccount, selectAccounts } from '@/queries/accounts/selectors'; import { useSettingsStore } from '@/stores/settings'; import { getDomain } from '@/utils/accounts'; -import { validId } from '@/utils/auth'; import ConfigDB from '@/utils/config-db'; import { shouldFilter } from '@/utils/timelines'; -import type { EntityStore } from '@/entity-store/types'; import type { minifyAdminReport } from '@/queries/utils/minify-list'; import type { MinifiedStatus } from '@/reducers/statuses'; import type { MRFSimple } from '@/schemas/pleroma'; import type { RootState } from '@/store'; import type { Account, Filter, FilterResult, NotificationGroup } from 'pl-api'; -const selectAccount = (state: RootState, accountId: string) => - state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined; - -const selectAccounts = (state: RootState, accountIds: Array) => - accountIds - .map((accountId) => state.entities[Entities.ACCOUNTS]?.store[accountId] as Account | undefined) - .filter((account): account is Account => account !== undefined); - -const selectOwnAccount = (state: RootState) => { - if (state.me) { - return selectAccount(state, state.me); - } -}; - const toServerSideType = (columnType: string): Filter['context'][0] => { switch (columnType) { case 'home': @@ -163,11 +147,11 @@ const makeGetNotification = () => (_state: RootState, notification: NotificationGroup) => notification, (state: RootState, notification: NotificationGroup) => // @ts-expect-error types will be fine valibot ensures that - selectAccount(state, notification.target_id), + selectAccount(notification.target_id), // @ts-expect-error types will be fine valibot ensures that (state: RootState, notification: NotificationGroup) => state.statuses[notification.status_id], (state: RootState, notification: NotificationGroup) => - selectAccounts(state, notification.sample_account_ids), + selectAccounts(notification.sample_account_ids), ], (notification, target, status, accounts): SelectedNotification => ({ ...notification, @@ -213,19 +197,18 @@ const makeGetReport = () => { return createSelector( [ (state: RootState, report?: ReturnType) => report, - (state: RootState, report?: ReturnType) => - selectAccount(state, report?.account_id ?? ''), - (state: RootState, report?: ReturnType) => - selectAccount(state, report?.target_account_id ?? ''), - (state: RootState, report?: ReturnType) => - selectAccount(state, report?.assigned_account_id ?? ''), (state: RootState, report?: ReturnType) => report?.status_ids .map((statusId) => getStatus(state, { id: statusId })) .filter((status): status is SelectedStatus => status !== null), ], - (report, account, target_account, assigned_account, statuses = []) => { + (report, statuses = []) => { if (!report) return null; + + const account = selectAccount(report.account_id ?? ''); + const target_account = selectAccount(report.target_account_id ?? ''); + const assigned_account = selectAccount(report.assigned_account_id ?? ''); + return { ...report, account, @@ -237,30 +220,6 @@ const makeGetReport = () => { ); }; -const getAuthUserIds = createSelector([(state: RootState) => state.auth.users], (authUsers) => - Object.values(authUsers).reduce((userIds: Array, authUser) => { - const userId = authUser?.id; - if (validId(userId)) userIds.push(userId); - return userIds; - }, []), -); - -const makeGetOtherAccounts = () => - createSelector( - [ - (state: RootState) => state.entities[Entities.ACCOUNTS]?.store as EntityStore, - getAuthUserIds, - (state: RootState) => state.me, - ], - (accounts, authUserIds, me) => - authUserIds.reduce>((list, id) => { - if (id === me) return list; - const account = accounts?.[id]; - if (account) list.push(account); - return list; - }, []), - ); - const getSimplePolicy = createSelector( [ (state: RootState) => state.admin.configs, @@ -272,11 +231,8 @@ const getSimplePolicy = createSelector( }), ); -const getRemoteInstanceFavicon = (state: RootState, host: string) => { - const accounts = state.entities[Entities.ACCOUNTS]?.store as EntityStore; - const account = Object.entries(accounts).find( - ([_, account]) => account && getDomain(account) === host, - )?.[1]; +const getRemoteInstanceFavicon = (_state: RootState, host: string) => { + const account = getAccounts().find((item) => item && getDomain(item) === host); return account?.favicon ?? null; }; @@ -344,16 +300,12 @@ const makeGetStatusIds = () => export { type RemoteInstance, - selectAccount, - selectAccounts, - selectOwnAccount, getFilters, regexFromFilters, makeGetStatus, type SelectedStatus, makeGetNotification, makeGetReport, - makeGetOtherAccounts, makeGetHosts, makeGetRemoteInstance, makeGetStatusIds, diff --git a/packages/pl-fe/src/utils/auth.ts b/packages/pl-fe/src/utils/auth.ts index 674ff65eb..f8c6e1e00 100644 --- a/packages/pl-fe/src/utils/auth.ts +++ b/packages/pl-fe/src/utils/auth.ts @@ -1,4 +1,4 @@ -import { selectAccount, selectOwnAccount } from '@/selectors'; +import { selectAccount, selectOwnAccount } from '@/queries/accounts/selectors'; import type { RootState } from '@/store'; @@ -28,7 +28,7 @@ const isLoggedIn = (getState: () => RootState) => validId(getState().me); const getUserToken = (state: RootState, accountId?: string | false | null) => { if (!accountId) return; - const accountUrl = selectAccount(state, accountId)?.url; + const accountUrl = selectAccount(accountId)?.url; if (!accountUrl) return; return state.auth.users[accountUrl]?.access_token; }; diff --git a/packages/pl-fe/src/utils/state.ts b/packages/pl-fe/src/utils/state.ts index 55899a31c..195feb3a2 100644 --- a/packages/pl-fe/src/utils/state.ts +++ b/packages/pl-fe/src/utils/state.ts @@ -6,7 +6,7 @@ import { getFrontendConfig } from '@/actions/frontend-config'; import * as BuildConfig from '@/build-config'; import { isPrerendered } from '@/precheck'; -import { selectOwnAccount } from '@/selectors'; +import { selectOwnAccount } from '@/queries/accounts/selectors'; import { isURL } from '@/utils/auth'; import type { RootState } from '@/store'; From c8abd676d9f07222905cec4a6250b8b724bc1906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 13:50:48 +0100 Subject: [PATCH 057/264] nicolium: remove markers actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/markers.ts | 36 --------------------------- 1 file changed, 36 deletions(-) delete mode 100644 packages/pl-fe/src/actions/markers.ts diff --git a/packages/pl-fe/src/actions/markers.ts b/packages/pl-fe/src/actions/markers.ts deleted file mode 100644 index 20a8f2750..000000000 --- a/packages/pl-fe/src/actions/markers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { getClient } from '../api'; - -import type { AppDispatch, RootState } from '@/store'; -import type { Markers, SaveMarkersParams } from 'pl-api'; - -const MARKER_FETCH_SUCCESS = 'MARKER_FETCH_SUCCESS' as const; - -const MARKER_SAVE_SUCCESS = 'MARKER_SAVE_SUCCESS' as const; - -const fetchMarker = - (timeline: Array) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState) - .timelines.getMarkers(timeline) - .then((marker) => { - dispatch({ type: MARKER_FETCH_SUCCESS, marker }); - }); - -const saveMarker = - (marker: SaveMarkersParams) => (dispatch: AppDispatch, getState: () => RootState) => - getClient(getState) - .timelines.saveMarkers(marker) - .then((marker) => { - dispatch({ type: MARKER_SAVE_SUCCESS, marker }); - }); - -type MarkersAction = - | { - type: typeof MARKER_FETCH_SUCCESS; - marker: Markers; - } - | { - type: typeof MARKER_SAVE_SUCCESS; - marker: Markers; - }; - -export { MARKER_FETCH_SUCCESS, MARKER_SAVE_SUCCESS, fetchMarker, saveMarker, type MarkersAction }; From 364a521f55a25816457af59b87a7e1476dd15cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 14:11:55 +0100 Subject: [PATCH 058/264] nicolium: fix alt text editing on mastodon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-api/lib/params/statuses.ts | 8 ++++++- packages/pl-fe/src/actions/compose.ts | 31 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/pl-api/lib/params/statuses.ts b/packages/pl-api/lib/params/statuses.ts index 9a8ef8e9c..577e7b0ea 100644 --- a/packages/pl-api/lib/params/statuses.ts +++ b/packages/pl-api/lib/params/statuses.ts @@ -164,7 +164,13 @@ type EditStatusOptionalParams = Pick< * @category Request params */ type EditStatusParams = (CreateStatusWithContent | CreateStatusWithMedia) & - EditStatusOptionalParams; + EditStatusOptionalParams & { + media_attributes?: Array<{ + id: string; + description?: string; + focus?: string; + }>; + }; /** * @category Request params diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts index c1c64057d..838c2e004 100644 --- a/packages/pl-fe/src/actions/compose.ts +++ b/packages/pl-fe/src/actions/compose.ts @@ -39,6 +39,7 @@ import type { InteractionPolicy, UpdateMediaParams, Location, + EditStatusParams, } from 'pl-api'; let cancelFetchComposeSuggestions = new AbortController(); @@ -515,9 +516,25 @@ const submitCompose = undefined, quote_approval_policy: compose.quoteApprovalPolicy ?? undefined, location_id: compose.location?.origin_id ?? undefined, - preview, }; + if (compose.editedId) { + // @ts-ignore + params.media_attributes = media.map((item) => { + const focalPoint = (item.type === 'image' || item.type === 'gifv') && item.meta?.focus; + + const focus = focalPoint + ? `${focalPoint.x.toFixed(2)},${focalPoint.y.toFixed(2)}` + : undefined; + + return { + id: item.id, + description: item.description, + focus, + }; + }) as EditStatusParams['media_attributes']; + } + if (compose.poll) { params.poll = { options: compose.poll.options, @@ -680,6 +697,8 @@ const changeUploadCompose = (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return Promise.resolve(); + const compose = getState().compose[composeId]; + dispatch(changeUploadComposeRequest(composeId)); return dispatch(updateMedia(mediaId, params)) @@ -688,6 +707,16 @@ const changeUploadCompose = return response; }) .catch((error) => { + if (error.response?.status === 404 && compose.editedId) { + // Editing an existing status. Mastodon doesn't let you update media attachments for already posted statuses. + // Pretend we got a success response. + const previousMedia = compose.mediaAttachments.find((m) => m.id === mediaId); + + if (previousMedia) { + dispatch(changeUploadComposeSuccess(composeId, { ...previousMedia, ...params })); + return; + } + } dispatch(changeUploadComposeFail(composeId, mediaId, error)); }); }; From c86050de3d4fb5c54e3eb07d9447102a8b0e3af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 16:10:09 +0100 Subject: [PATCH 059/264] nicolium: why????? MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/ui/button/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/pl-fe/src/components/ui/button/index.tsx b/packages/pl-fe/src/components/ui/button/index.tsx index 7f70b5fa8..32d69acba 100644 --- a/packages/pl-fe/src/components/ui/button/index.tsx +++ b/packages/pl-fe/src/components/ui/button/index.tsx @@ -127,7 +127,6 @@ const Button = React.forwardRef( href={href} target='_blank' rel='noopener' - tabIndex={-1} > {buttonChildren} From ace69346575824fb644c0e7bcefd9e44821f72ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 16:27:33 +0100 Subject: [PATCH 060/264] nicolium: fix regression from notifications update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/columns/notifications.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 94cb69b1e..957ad8d85 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -17,6 +17,7 @@ import Notification from '@/features/notifications/components/notification'; import PlaceholderNotification from '@/features/placeholder/components/placeholder-notification'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; +import { queryClient } from '@/queries/client'; import { type FilterType, useMarkNotificationsReadMutation, @@ -65,6 +66,9 @@ const FilterBar = () => { const onClick = (filterType: FilterType) => () => { changeSetting(['notifications', 'quickFilter', 'active'], filterType); dispatch(saveSettings()); + if (filterType === selectedFilter) { + queryClient.refetchQueries({ queryKey: ['notifications', filterType], exact: true }); + } }; const items: Item[] = [ From 2d930dd77f17f01dec8fb71e0a7fc90c48811a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 16:40:30 +0100 Subject: [PATCH 061/264] nicolium: never decrease notification arker last_read_id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/queries/notifications/use-notifications.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/pl-fe/src/queries/notifications/use-notifications.ts b/packages/pl-fe/src/queries/notifications/use-notifications.ts index a71b4d33b..e20678e0d 100644 --- a/packages/pl-fe/src/queries/notifications/use-notifications.ts +++ b/packages/pl-fe/src/queries/notifications/use-notifications.ts @@ -34,6 +34,7 @@ import { minifyGroupedNotifications } from '../utils/minify-list'; import type { GetGroupedNotificationsParams, + Marker, Notification, NotificationGroup, PaginatedResponse, @@ -243,6 +244,11 @@ const useMarkNotificationsReadMutation = () => { mutationFn: async (lastReadId?: string | null) => { if (!lastReadId) return; + const currentMarker = queryClient.getQueryData(['markers', 'notifications']); + if (currentMarker && compareId(currentMarker.last_read_id, lastReadId) >= 0) { + return; + } + return await client.timelines.saveMarkers({ notifications: { last_read_id: lastReadId, From e3b1019c25453efe5ccd3a5f36ae9db5008ae11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 16:40:30 +0100 Subject: [PATCH 062/264] nicolium: this should never be reachable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../queries/notifications/use-notifications.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/pl-fe/src/queries/notifications/use-notifications.ts b/packages/pl-fe/src/queries/notifications/use-notifications.ts index a71b4d33b..5f6ec45b8 100644 --- a/packages/pl-fe/src/queries/notifications/use-notifications.ts +++ b/packages/pl-fe/src/queries/notifications/use-notifications.ts @@ -34,6 +34,7 @@ import { minifyGroupedNotifications } from '../utils/minify-list'; import type { GetGroupedNotificationsParams, + Marker, Notification, NotificationGroup, PaginatedResponse, @@ -243,6 +244,11 @@ const useMarkNotificationsReadMutation = () => { mutationFn: async (lastReadId?: string | null) => { if (!lastReadId) return; + const currentMarker = queryClient.getQueryData(['markers', 'notifications']); + if (currentMarker && compareId(currentMarker.last_read_id, lastReadId) >= 0) { + return; + } + return await client.timelines.saveMarkers({ notifications: { last_read_id: lastReadId, @@ -252,18 +258,7 @@ const useMarkNotificationsReadMutation = () => { onSuccess: (markers, lastReadId) => { if (markers?.notifications) { queryClient.setQueryData(['markers', 'notifications'], markers.notifications); - return; } - - if (!lastReadId) return; - - queryClient.setQueryData(['markers', 'notifications'], (marker) => { - if (!marker) return undefined; - return { - ...marker, - last_read_id: lastReadId, - }; - }); }, }); }; From bf174860597d676fd50c951b1b0c22c5ee55dc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 16:44:03 +0100 Subject: [PATCH 063/264] nicolium: fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/columns/notifications.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 957ad8d85..563d4da65 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -293,6 +293,10 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => }; }, []); + useEffect(() => { + setTopNotification(undefined); + }, [activeFilter]); + useEffect(() => { if (topNotification || displayedNotifications.length === 0) return; setTopNotification(displayedNotifications[0].most_recent_notification_id); From 2a081ec1b5c92c12be8840816df49ab68e549be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 18:34:39 +0100 Subject: [PATCH 064/264] nicolium: add a comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/columns/notifications.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 563d4da65..5be3e9db6 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -275,6 +275,8 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => queryClient.setQueryData>(['notifications', activeFilter], (data) => { if (!data) return data; + // from https://github.com/TanStack/query/discussions/875#discussioncomment-754458 + // TODO: maybe needed in more places so maybe make a helper for this return { ...data, pages: data.pages.slice(0, 1), From ddbf26d9fd46021a55d40ad6a6f2f18035145b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 21:33:25 +0100 Subject: [PATCH 065/264] nicolium: possibly improve notifications behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/columns/notifications.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/pl-fe/src/columns/notifications.tsx b/packages/pl-fe/src/columns/notifications.tsx index 5be3e9db6..47dda7c5d 100644 --- a/packages/pl-fe/src/columns/notifications.tsx +++ b/packages/pl-fe/src/columns/notifications.tsx @@ -225,6 +225,7 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => }, [notifications, topNotification]); const hasMore = hasNextPage ?? false; + const isFirstRender = useRef(true); const node = useRef(null); const scrollableContentRef = useRef | null>(null); @@ -266,7 +267,7 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => }; const handleDequeueNotifications = useCallback(() => { - setTopNotification(undefined); + setTopNotification(notifications[0]?.most_recent_notification_id); markNotificationsRead(notifications[0]?.most_recent_notification_id); }, [notifications, markNotificationsRead]); @@ -296,6 +297,10 @@ const NotificationsColumn: React.FC = ({ multiColumn }) => }, []); useEffect(() => { + if (isFirstRender.current) { + isFirstRender.current = false; + return; + } setTopNotification(undefined); }, [activeFilter]); From 6dd49787874d599062cf611c15e65f8301252e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 21:41:34 +0100 Subject: [PATCH 066/264] nicolium: oh fuck MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/queries/accounts/use-account.ts | 2 +- packages/pl-fe/src/queries/accounts/use-accounts.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pl-fe/src/queries/accounts/use-account.ts b/packages/pl-fe/src/queries/accounts/use-account.ts index c7c65eb85..ad79e3f38 100644 --- a/packages/pl-fe/src/queries/accounts/use-account.ts +++ b/packages/pl-fe/src/queries/accounts/use-account.ts @@ -34,7 +34,7 @@ const useAccount = (accountId?: string, withRelationship = false) => { queryKey: ['accounts', accountId], queryFn: async () => { const account = await client.accounts.getAccount(accountId!); - queryClient.setQueryData(['accounts', 'lookup', account.acct.toLowerCase()], account); + queryClient.setQueryData(['accounts', 'lookup', account.acct.toLowerCase()], account.id); return account; }, enabled: !!accountId, diff --git a/packages/pl-fe/src/queries/accounts/use-accounts.ts b/packages/pl-fe/src/queries/accounts/use-accounts.ts index c71267a9c..8c91a5775 100644 --- a/packages/pl-fe/src/queries/accounts/use-accounts.ts +++ b/packages/pl-fe/src/queries/accounts/use-accounts.ts @@ -14,7 +14,7 @@ const useAccounts = (accountIds: Array) => { queryKey: ['accounts', accountId], queryFn: async () => { const response = await client.accounts.getAccount(accountId); - queryClient.setQueryData(['accounts', 'lookup', response.acct.toLowerCase()], response); + queryClient.setQueryData(['accounts', 'lookup', response.acct.toLowerCase()], response.id); return response; }, enabled: !!accountId, From 8ac76b43e315d15f8dd2dfb5a0e67143ee0288ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 21:42:16 +0100 Subject: [PATCH 067/264] nicolium: need to start annotating types for setQueryData moar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/queries/accounts/use-account.ts | 5 ++++- packages/pl-fe/src/queries/accounts/use-accounts.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/pl-fe/src/queries/accounts/use-account.ts b/packages/pl-fe/src/queries/accounts/use-account.ts index ad79e3f38..05fd1a89a 100644 --- a/packages/pl-fe/src/queries/accounts/use-account.ts +++ b/packages/pl-fe/src/queries/accounts/use-account.ts @@ -34,7 +34,10 @@ const useAccount = (accountId?: string, withRelationship = false) => { queryKey: ['accounts', accountId], queryFn: async () => { const account = await client.accounts.getAccount(accountId!); - queryClient.setQueryData(['accounts', 'lookup', account.acct.toLowerCase()], account.id); + queryClient.setQueryData( + ['accounts', 'lookup', account.acct.toLowerCase()], + account.id, + ); return account; }, enabled: !!accountId, diff --git a/packages/pl-fe/src/queries/accounts/use-accounts.ts b/packages/pl-fe/src/queries/accounts/use-accounts.ts index 8c91a5775..eb812279b 100644 --- a/packages/pl-fe/src/queries/accounts/use-accounts.ts +++ b/packages/pl-fe/src/queries/accounts/use-accounts.ts @@ -14,7 +14,10 @@ const useAccounts = (accountIds: Array) => { queryKey: ['accounts', accountId], queryFn: async () => { const response = await client.accounts.getAccount(accountId); - queryClient.setQueryData(['accounts', 'lookup', response.acct.toLowerCase()], response.id); + queryClient.setQueryData( + ['accounts', 'lookup', response.acct.toLowerCase()], + response.id, + ); return response; }, enabled: !!accountId, From c577a182f1ea65da87455434c0e5e1f25791e2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 22:41:45 +0100 Subject: [PATCH 068/264] nicolium: migrate compose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/admin.ts | 20 +- packages/pl-fe/src/actions/compose.ts | 1374 ----------------- packages/pl-fe/src/actions/events.ts | 40 +- packages/pl-fe/src/actions/instance.ts | 2 + packages/pl-fe/src/actions/me.ts | 3 + packages/pl-fe/src/actions/statuses.ts | 65 +- packages/pl-fe/src/actions/timelines.ts | 2 + .../src/components/autosuggest-input.tsx | 2 +- packages/pl-fe/src/components/modal-root.tsx | 122 +- .../src/components/status-action-bar.tsx | 28 +- packages/pl-fe/src/components/status.tsx | 7 +- .../pl-fe/src/components/thumb-navigation.tsx | 11 +- .../features/account/components/header.tsx | 7 +- .../compose-event/tabs/edit-event.tsx | 27 +- .../compose/components/compose-form.tsx | 81 +- .../components/content-type-button.tsx | 12 +- .../compose/components/drive-button.tsx | 9 +- .../components/hashtag-casing-suggestion.tsx | 11 +- .../compose/components/language-dropdown.tsx | 36 +- .../compose/components/location-button.tsx | 17 +- .../compose/components/location-form.tsx | 12 +- .../compose/components/poll-button.tsx | 14 +- .../compose/components/polls/poll-form.tsx | 73 +- .../compose/components/privacy-dropdown.tsx | 17 +- .../components/reply-group-indicator.tsx | 6 +- .../compose/components/schedule-button.tsx | 14 +- .../compose/components/schedule-form.tsx | 15 +- .../components/sensitive-media-button.tsx | 11 +- .../compose/components/spoiler-input.tsx | 55 +- .../compose/components/upload-form.tsx | 13 +- .../features/compose/components/upload.tsx | 27 +- .../containers/preview-compose-container.tsx | 10 +- .../containers/quoted-status-container.tsx | 15 +- .../containers/reply-indicator-container.tsx | 8 +- .../containers/upload-button-container.tsx | 8 +- .../src/features/compose/editor/index.tsx | 11 +- .../editor/plugins/autosuggest-plugin.tsx | 75 +- .../floating-block-type-toolbar-plugin.tsx | 2 +- .../compose/editor/plugins/state-plugin.tsx | 163 +- .../components/draft-status-action-bar.tsx | 16 +- .../event/components/event-header.tsx | 9 +- .../notifications/components/notification.tsx | 11 +- .../src/features/reply-mentions/account.tsx | 21 +- .../src/features/status/components/thread.tsx | 11 +- .../features/ui/components/compose-button.tsx | 7 +- .../src/features/ui/components/modal-root.tsx | 6 +- .../src/features/ui/util/global-hotkeys.tsx | 7 +- .../src/hooks/use-compose-suggestions.ts | 54 + packages/pl-fe/src/hooks/use-compose.ts | 11 +- packages/pl-fe/src/layouts/home-layout.tsx | 11 +- .../compose-interaction-policy-modal.tsx | 32 +- packages/pl-fe/src/modals/compose-modal.tsx | 13 +- .../pl-fe/src/modals/reply-mentions-modal.tsx | 2 +- packages/pl-fe/src/pages/fun/circle.tsx | 10 +- .../src/pages/statuses/compose-event.tsx | 7 +- .../src/pages/statuses/event-discussion.tsx | 5 +- .../src/pages/timelines/group-timeline.tsx | 18 +- packages/pl-fe/src/pages/utils/share.tsx | 7 +- .../queries/statuses/use-draft-statuses.ts | 27 +- .../make-paginated-response-query-options.ts | 5 +- packages/pl-fe/src/reducers/compose.ts | 888 ----------- packages/pl-fe/src/reducers/index.ts | 2 - packages/pl-fe/src/stores/compose.ts | 1038 +++++++++++++ 63 files changed, 1739 insertions(+), 2904 deletions(-) delete mode 100644 packages/pl-fe/src/actions/compose.ts create mode 100644 packages/pl-fe/src/hooks/use-compose-suggestions.ts delete mode 100644 packages/pl-fe/src/reducers/compose.ts create mode 100644 packages/pl-fe/src/stores/compose.ts diff --git a/packages/pl-fe/src/actions/admin.ts b/packages/pl-fe/src/actions/admin.ts index 6c99d761d..b7b23893c 100644 --- a/packages/pl-fe/src/actions/admin.ts +++ b/packages/pl-fe/src/actions/admin.ts @@ -1,11 +1,11 @@ import { importEntities } from '@/actions/importer'; import { queryClient } from '@/queries/client'; +import { useComposeStore } from '@/stores/compose'; import { useModalsStore } from '@/stores/modals'; import { filterBadges, getTagDiff } from '@/utils/badges'; import { getClient } from '../api'; -import { setComposeToStatus } from './compose'; import { STATUS_FETCH_SOURCE_FAIL, type StatusesAction } from './statuses'; import { deleteFromTimelines } from './timelines'; @@ -144,20 +144,10 @@ const redactStatus = (statusId: string) => (dispatch: AppDispatch, getState: () return getClient(state) .statuses.getStatusSource(statusId) - .then((response) => { - dispatch( - setComposeToStatus( - status, - poll, - response.text, - response.spoiler_text, - response.content_type, - false, - undefined, - undefined, - true, - ), - ); + .then((source) => { + useComposeStore + .getState() + .actions.setComposeToStatus(status, poll, source, false, null, null, true); useModalsStore.getState().actions.openModal('COMPOSE'); }) .catch((error) => { diff --git a/packages/pl-fe/src/actions/compose.ts b/packages/pl-fe/src/actions/compose.ts deleted file mode 100644 index 838c2e004..000000000 --- a/packages/pl-fe/src/actions/compose.ts +++ /dev/null @@ -1,1374 +0,0 @@ -import throttle from 'lodash/throttle'; -import { defineMessages, IntlShape } from 'react-intl'; - -import { getClient } from '@/api'; -import { isNativeEmoji } from '@/features/emoji'; -import emojiSearch from '@/features/emoji/search'; -import { Language } from '@/features/preferences'; -import { selectAccount, selectOwnAccount } from '@/queries/accounts/selectors'; -import { queryClient } from '@/queries/client'; -import { cancelDraftStatus } from '@/queries/statuses/use-draft-statuses'; -import { useModalsStore } from '@/stores/modals'; -import { useSettingsStore } from '@/stores/settings'; -import toast from '@/toast'; -import { isLoggedIn } from '@/utils/auth'; - -import { importEntities } from './importer'; -import { uploadFile, updateMedia } from './media'; -import { saveSettings } from './settings'; -import { createStatus } from './statuses'; - -import type { AutoSuggestion } from '@/components/autosuggest-input'; -import type { Emoji } from '@/features/emoji'; -import type { Status } from '@/normalizers/status'; -import type { Policy, Rule, Scope } from '@/pages/settings/interaction-policies'; -import type { ClearLinkSuggestion } from '@/reducers/compose'; -import type { AppDispatch, RootState } from '@/store'; -import type { LinkOptions } from '@tanstack/react-router'; -import type { EditorState } from 'lexical'; -import type { - Account, - CreateStatusParams, - CustomEmoji, - Group, - MediaAttachment, - Status as BaseStatus, - Tag, - Poll, - ScheduledStatus, - InteractionPolicy, - UpdateMediaParams, - Location, - EditStatusParams, -} from 'pl-api'; - -let cancelFetchComposeSuggestions = new AbortController(); - -const COMPOSE_CHANGE = 'COMPOSE_CHANGE' as const; -const COMPOSE_SUBMIT_REQUEST = 'COMPOSE_SUBMIT_REQUEST' as const; -const COMPOSE_SUBMIT_SUCCESS = 'COMPOSE_SUBMIT_SUCCESS' as const; -const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL' as const; -const COMPOSE_PREVIEW_SUCCESS = 'COMPOSE_PREVIEW_SUCCESS' as const; -const COMPOSE_PREVIEW_CANCEL = 'COMPOSE_PREVIEW_CANCEL' as const; -const COMPOSE_REPLY = 'COMPOSE_REPLY' as const; -const COMPOSE_EVENT_REPLY = 'COMPOSE_EVENT_REPLY' as const; -const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL' as const; -const COMPOSE_QUOTE = 'COMPOSE_QUOTE' as const; -const COMPOSE_QUOTE_CANCEL = 'COMPOSE_QUOTE_CANCEL' as const; -const COMPOSE_DIRECT = 'COMPOSE_DIRECT' as const; -const COMPOSE_MENTION = 'COMPOSE_MENTION' as const; -const COMPOSE_RESET = 'COMPOSE_RESET' as const; -const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST' as const; -const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS' as const; -const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL' as const; -const COMPOSE_UPLOAD_PROGRESS = 'COMPOSE_UPLOAD_PROGRESS' as const; -const COMPOSE_UPLOAD_UNDO = 'COMPOSE_UPLOAD_UNDO' as const; -const COMPOSE_GROUP_POST = 'COMPOSE_GROUP_POST' as const; - -const COMPOSE_SUGGESTIONS_CLEAR = 'COMPOSE_SUGGESTIONS_CLEAR' as const; -const COMPOSE_SUGGESTIONS_READY = 'COMPOSE_SUGGESTIONS_READY' as const; -const COMPOSE_SUGGESTION_SELECT = 'COMPOSE_SUGGESTION_SELECT' as const; -const COMPOSE_SUGGESTION_TAGS_UPDATE = 'COMPOSE_SUGGESTION_TAGS_UPDATE' as const; - -const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE' as const; -const COMPOSE_TYPE_CHANGE = 'COMPOSE_TYPE_CHANGE' as const; -const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE' as const; -const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE' as const; -const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE' as const; -const COMPOSE_MODIFIED_LANGUAGE_CHANGE = 'COMPOSE_MODIFIED_LANGUAGE_CHANGE' as const; -const COMPOSE_LANGUAGE_ADD = 'COMPOSE_LANGUAGE_ADD' as const; -const COMPOSE_LANGUAGE_DELETE = 'COMPOSE_LANGUAGE_DELETE' as const; -const COMPOSE_FEDERATED_CHANGE = 'COMPOSE_FEDERATED_CHANGE' as const; - -const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST' as const; -const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS' as const; -const COMPOSE_UPLOAD_CHANGE_FAIL = 'COMPOSE_UPLOAD_UPDATE_FAIL' as const; - -const COMPOSE_POLL_ADD = 'COMPOSE_POLL_ADD' as const; -const COMPOSE_POLL_REMOVE = 'COMPOSE_POLL_REMOVE' as const; -const COMPOSE_POLL_OPTION_ADD = 'COMPOSE_POLL_OPTION_ADD' as const; -const COMPOSE_POLL_OPTION_CHANGE = 'COMPOSE_POLL_OPTION_CHANGE' as const; -const COMPOSE_POLL_OPTION_REMOVE = 'COMPOSE_POLL_OPTION_REMOVE' as const; -const COMPOSE_POLL_SETTINGS_CHANGE = 'COMPOSE_POLL_SETTINGS_CHANGE' as const; - -const COMPOSE_SCHEDULE_ADD = 'COMPOSE_SCHEDULE_ADD' as const; -const COMPOSE_SCHEDULE_SET = 'COMPOSE_SCHEDULE_SET' as const; -const COMPOSE_SCHEDULE_REMOVE = 'COMPOSE_SCHEDULE_REMOVE' as const; - -const COMPOSE_ADD_TO_MENTIONS = 'COMPOSE_ADD_TO_MENTIONS' as const; -const COMPOSE_REMOVE_FROM_MENTIONS = 'COMPOSE_REMOVE_FROM_MENTIONS' as const; - -const COMPOSE_SET_STATUS = 'COMPOSE_SET_STATUS' as const; - -const COMPOSE_EDITOR_STATE_SET = 'COMPOSE_EDITOR_STATE_SET' as const; - -const COMPOSE_CHANGE_MEDIA_ORDER = 'COMPOSE_CHANGE_MEDIA_ORDER' as const; - -const COMPOSE_ADD_SUGGESTED_QUOTE = 'COMPOSE_ADD_SUGGESTED_QUOTE' as const; -const COMPOSE_ADD_SUGGESTED_LANGUAGE = 'COMPOSE_ADD_SUGGESTED_LANGUAGE' as const; - -const COMPOSE_INTERACTION_POLICY_OPTION_CHANGE = - 'COMPOSE_INTERACTION_POLICY_OPTION_CHANGE' as const; -const COMPOSE_QUOTE_POLICY_OPTION_CHANGE = 'COMPOSE_QUOTE_POLICY_OPTION_CHANGE' as const; - -const COMPOSE_CLEAR_LINK_SUGGESTION_CREATE = 'COMPOSE_CLEAR_LINK_SUGGESTION_CREATE' as const; -const COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE = 'COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE' as const; - -const COMPOSE_HASHTAG_CASING_SUGGESTION_SET = 'COMPOSE_HASHTAG_CASING_SUGGESTION_SET' as const; -const COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE = - 'COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE' as const; - -const COMPOSE_REDACTING_OVERWRITE_CHANGE = 'COMPOSE_REDACTING_OVERWRITE_CHANGE' as const; - -const COMPOSE_SET_LOCATION = 'COMPOSE_SET_LOCATION' as const; -const COMPOSE_SET_SHOW_LOCATION_PICKER = 'COMPOSE_SET_SHOW_LOCATION_PICKER' as const; - -const messages = defineMessages({ - scheduleError: { - id: 'compose.invalid_schedule', - defaultMessage: 'You must schedule a post at least 5 minutes out.', - }, - success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' }, - editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' }, - redactSuccess: { id: 'compose.redact_success', defaultMessage: 'The post was redacted' }, - scheduledSuccess: { id: 'compose.scheduled_success', defaultMessage: 'Your post was scheduled' }, - uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, - uploadErrorPoll: { - id: 'upload_error.poll', - defaultMessage: 'File upload not allowed with polls.', - }, - view: { id: 'toast.view', defaultMessage: 'View' }, - replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, - replyMessage: { - id: 'confirmations.reply.message', - defaultMessage: - 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?', - }, -}); - -interface ComposeSetStatusAction { - type: typeof COMPOSE_SET_STATUS; - composeId: string; - status: Pick< - Status, - | 'id' - | 'account' - | 'content' - | 'group_id' - | 'in_reply_to_id' - | 'language' - | 'media_attachments' - | 'mentions' - | 'quote_id' - | 'sensitive' - | 'spoiler_text' - | 'visibility' - >; - poll?: Poll | null; - rawText: string; - explicitAddressing: boolean; - spoilerText?: string; - contentType?: string | false; - withRedraft?: boolean; - draftId?: string; - editorState?: string | null; - redacting?: boolean; -} - -const setComposeToStatus = - ( - status: ComposeSetStatusAction['status'], - poll: Poll | null | undefined, - rawText: string, - spoilerText?: string, - contentType?: string | false, - withRedraft?: boolean, - draftId?: string, - editorState?: string | null, - redacting?: boolean, - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const { features } = getClient(getState); - const explicitAddressing = - features.createStatusExplicitAddressing && - !useSettingsStore.getState().settings.forceImplicitAddressing; - - dispatch({ - type: COMPOSE_SET_STATUS, - composeId: 'compose-modal', - status, - poll, - rawText, - explicitAddressing, - spoilerText, - contentType, - withRedraft, - draftId, - editorState, - redacting, - }); - }; - -const changeCompose = (composeId: string, text: string) => ({ - type: COMPOSE_CHANGE, - composeId, - text: text, -}); - -interface ComposeReplyAction { - type: typeof COMPOSE_REPLY; - composeId: string; - status: Pick< - Status, - | 'id' - | 'account' - | 'group_id' - | 'list_id' - | 'local_only' - | 'mentions' - | 'spoiler_text' - | 'visibility' - >; - account: Pick; - explicitAddressing: boolean; - preserveSpoilers: boolean; - rebloggedBy?: Pick; - approvalRequired?: boolean; - conversationScope: boolean; -} - -const replyCompose = - ( - status: ComposeReplyAction['status'], - rebloggedBy?: ComposeReplyAction['rebloggedBy'], - approvalRequired?: ComposeReplyAction['approvalRequired'], - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const { features } = getClient(getState); - const { forceImplicitAddressing, preserveSpoilers } = useSettingsStore.getState().settings; - const explicitAddressing = features.createStatusExplicitAddressing && !forceImplicitAddressing; - const account = selectOwnAccount(state); - - if (!account) return; - - dispatch({ - type: COMPOSE_REPLY, - composeId: 'compose-modal', - status, - account, - explicitAddressing, - preserveSpoilers, - rebloggedBy, - approvalRequired, - conversationScope: features.createStatusConversationScope, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); - }; - -const cancelReplyCompose = () => ({ - type: COMPOSE_REPLY_CANCEL, - composeId: 'compose-modal', -}); - -interface ComposeQuoteAction { - type: typeof COMPOSE_QUOTE; - composeId: string; - status: Pick; - account: Pick | undefined; - explicitAddressing: boolean; - conversationScope: boolean; - approvalRequired?: boolean; -} - -const quoteCompose = - (status: ComposeQuoteAction['status'], approvalRequired?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const { createStatusConversationScope, createStatusExplicitAddressing } = - state.auth.client.features; - const explicitAddressing = createStatusExplicitAddressing && !forceImplicitAddressing; - - dispatch({ - type: COMPOSE_QUOTE, - composeId: 'compose-modal', - status, - account: selectOwnAccount(state), - explicitAddressing, - conversationScope: createStatusConversationScope, - approvalRequired, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); - }; - -const cancelQuoteCompose = (composeId: string) => ({ - type: COMPOSE_QUOTE_CANCEL, - composeId, -}); - -const groupComposeModal = (group: Pick) => (dispatch: AppDispatch) => { - const composeId = `group:${group.id}`; - - dispatch(groupCompose(composeId, group.id)); - useModalsStore.getState().actions.openModal('COMPOSE', { composeId }); -}; - -const resetCompose = (composeId = 'compose-modal') => ({ - type: COMPOSE_RESET, - composeId, -}); - -interface ComposeMentionAction { - type: typeof COMPOSE_MENTION; - composeId: string; - account: Pick; -} - -const mentionCompose = - (account: ComposeMentionAction['account']) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!getState().me) return; - - dispatch({ - type: COMPOSE_MENTION, - composeId: 'compose-modal', - account: account, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); - }; - -interface ComposeDirectAction { - type: typeof COMPOSE_DIRECT; - composeId: string; - account: Pick; -} - -const directCompose = (account: ComposeDirectAction['account']) => (dispatch: AppDispatch) => { - dispatch({ - type: COMPOSE_DIRECT, - composeId: 'compose-modal', - account, - }); - useModalsStore.getState().actions.openModal('COMPOSE'); -}; - -const handleComposeSubmit = ( - dispatch: AppDispatch, - getState: () => RootState, - composeId: string, - data: BaseStatus | ScheduledStatus, - status: string, - edit?: boolean, - redact?: boolean, -) => { - if (!dispatch || !getState) return; - - const state = getState(); - - const accountUrl = selectOwnAccount(state)!.url; - const draftId = getState().compose[composeId].draftId; - - dispatch(submitComposeSuccess(composeId, data)); - - if (draftId) { - cancelDraftStatus(queryClient, accountUrl, draftId); - } - - if (data.scheduled_at === null) { - const linkOptions: LinkOptions = - data.visibility === 'direct' && getClient(getState()).features.conversations - ? { to: '/conversations' } - : { - to: '/@{$username}/posts/$statusId', - params: { username: data.account.acct, statusId: data.id }, - }; - toast.success( - redact ? messages.redactSuccess : edit ? messages.editSuccess : messages.success, - { - actionLabel: messages.view, - actionLinkOptions: linkOptions, - }, - ); - } else { - toast.success(messages.scheduledSuccess, { - actionLabel: messages.view, - actionLinkOptions: { to: '/scheduled_statuses' }, - }); - } -}; - -const needsDescriptions = (state: RootState, composeId: string) => { - const media = state.compose[composeId].mediaAttachments; - const missingDescriptionModal = useSettingsStore.getState().settings.missingDescriptionModal; - - const hasMissing = media.filter((item) => !item.description).length > 0; - - return missingDescriptionModal && hasMissing; -}; - -const validateSchedule = (state: RootState, composeId: string) => { - const scheduledAt = state.compose[composeId]?.scheduledAt; - if (!scheduledAt) return true; - - const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); - - return ( - scheduledAt.getTime() > fiveMinutesFromNow.getTime() || - (state.auth.client.features.scheduledStatusesBackwards && - scheduledAt.getTime() < new Date().getTime()) - ); -}; - -interface SubmitComposeOpts { - force?: boolean; - onSuccess?: () => void; -} - -const submitCompose = - (composeId: string, opts: SubmitComposeOpts = {}, preview = false) => - async (dispatch: AppDispatch, getState: () => RootState) => { - const { force = false, onSuccess } = opts; - - if (!isLoggedIn(getState)) return; - const state = getState(); - - const compose = state.compose[composeId]; - - const status = compose.text; - const media = compose.mediaAttachments; - const editedId = compose.editedId; - let to = compose.to; - const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const explicitAddressing = - state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; - - if (!preview) { - if (!validateSchedule(state, composeId)) { - toast.error(messages.scheduleError); - return; - } - - if ((!status || !status.length) && media.length === 0) { - return; - } - - if (!force && needsDescriptions(state, composeId)) { - useModalsStore.getState().actions.openModal('MISSING_DESCRIPTION', { - onContinue: () => { - useModalsStore.getState().actions.closeModal('MISSING_DESCRIPTION'); - dispatch(submitCompose(composeId, { force: true, onSuccess })); - }, - }); - return; - } - } - - // https://stackoverflow.com/a/30007882 for domain regex - const mentions: string[] | null = status.match( - /(?:^|\s)@([a-z\d_-]+(?:@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]+)?)/gi, - ); - - if (mentions) { - to = [ - ...new Set([ - ...to, - ...mentions.map((mention) => - mention - .replace(/ /g, '') - .trim() - .slice(1), - ), - ]), - ]; - } - - if (!preview) { - dispatch(submitComposeRequest(composeId)); - - useModalsStore.getState().actions.closeModal('COMPOSE'); - - if (compose.language && !editedId && !preview) { - useSettingsStore.getState().actions.rememberLanguageUse(compose.language); - dispatch(saveSettings()); - } - } - - const idempotencyKey = compose.idempotencyKey; - const contentType = compose.contentType === 'wysiwyg' ? 'text/markdown' : compose.contentType; - - const params: CreateStatusParams = { - status, - in_reply_to_id: compose.inReplyToId ?? undefined, - quote_id: compose.quoteId ?? undefined, - media_ids: media.map((item) => item.id), - sensitive: compose.sensitive, - spoiler_text: compose.spoilerText, - visibility: compose.visibility, - content_type: contentType, - scheduled_at: preview ? undefined : compose.scheduledAt?.toISOString(), - language: compose.language ?? compose.suggestedLanguage ?? undefined, - to: explicitAddressing && to.length ? to : undefined, - local_only: compose.localOnly, - interaction_policy: - (['public', 'unlisted', 'private'].includes(compose.visibility) && - compose.interactionPolicy) || - undefined, - quote_approval_policy: compose.quoteApprovalPolicy ?? undefined, - location_id: compose.location?.origin_id ?? undefined, - }; - - if (compose.editedId) { - // @ts-ignore - params.media_attributes = media.map((item) => { - const focalPoint = (item.type === 'image' || item.type === 'gifv') && item.meta?.focus; - - const focus = focalPoint - ? `${focalPoint.x.toFixed(2)},${focalPoint.y.toFixed(2)}` - : undefined; - - return { - id: item.id, - description: item.description, - focus, - }; - }) as EditStatusParams['media_attributes']; - } - - if (compose.poll) { - params.poll = { - options: compose.poll.options, - expires_in: compose.poll.expires_in, - multiple: compose.poll.multiple, - hide_totals: compose.poll.hide_totals, - options_map: compose.poll.options_map, - }; - } - - if (compose.language && Object.keys(compose.textMap).length) { - params.status_map = compose.textMap; - params.status_map[compose.language] = status; - - if (params.spoiler_text) { - params.spoiler_text_map = compose.spoilerTextMap; - params.spoiler_text_map[compose.language] = compose.spoilerText; - } - - const poll = params.poll; - if (poll?.options_map) { - poll.options.forEach( - (option, index: number) => (poll.options_map![index][compose.language!] = option), - ); - } - } - - if (compose.visibility === 'group' && compose.groupId) { - params.group_id = compose.groupId; - } - - if (preview) { - const data = await getClient(state).statuses.previewStatus(params); - dispatch(previewComposeSuccess(composeId, data)); - onSuccess?.(); - } else { - if (compose.redacting) { - // @ts-ignore - params.overwrite = compose.redactingOverwrite; - } - - try { - const data = await dispatch( - createStatus(params, idempotencyKey, editedId, compose.redacting), - ); - handleComposeSubmit( - dispatch, - getState, - composeId, - data, - status, - !!editedId, - compose.redacting, - ); - onSuccess?.(); - } catch (error) { - dispatch(submitComposeFail(composeId, error)); - } - } - }; - -const submitComposeRequest = (composeId: string) => ({ - type: COMPOSE_SUBMIT_REQUEST, - composeId, -}); - -const submitComposeSuccess = (composeId: string, status: BaseStatus | ScheduledStatus) => ({ - type: COMPOSE_SUBMIT_SUCCESS, - composeId, - status, -}); - -const submitComposeFail = (composeId: string, error: unknown) => ({ - type: COMPOSE_SUBMIT_FAIL, - composeId, - error, -}); - -const previewComposeSuccess = (composeId: string, status: Partial) => ({ - type: COMPOSE_PREVIEW_SUCCESS, - composeId, - status, -}); - -const cancelPreviewCompose = (composeId: string) => ({ - type: COMPOSE_PREVIEW_CANCEL, - composeId, -}); - -const uploadCompose = - (composeId: string, files: FileList, intl: IntlShape) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return; - const attachmentLimit = getState().instance.configuration.statuses.max_media_attachments; - - const media = getState().compose[composeId]?.mediaAttachments; - const progress = new Array(files.length).fill(0); - let total = Array.from(files).reduce((a, v) => a + v.size, 0); - - const mediaCount = media ? media.length : 0; - - if (files.length + mediaCount > attachmentLimit) { - toast.error(messages.uploadErrorLimit); - return; - } - - dispatch(uploadComposeRequest(composeId)); - - Array.from(files).forEach((f, i) => { - if (mediaCount + i > attachmentLimit - 1) return; - - dispatch( - uploadFile( - f, - intl, - (data) => dispatch(uploadComposeSuccess(composeId, data)), - (error) => dispatch(uploadComposeFail(composeId, error)), - ({ loaded }) => { - progress[i] = loaded; - dispatch( - uploadComposeProgress( - composeId, - progress.reduce((a, v) => a + v, 0), - total, - ), - ); - }, - (value) => (total += value), - ), - ); - }); - }; - -const uploadComposeRequest = (composeId: string) => ({ - type: COMPOSE_UPLOAD_REQUEST, - composeId, -}); - -const uploadComposeProgress = (composeId: string, loaded: number, total: number) => ({ - type: COMPOSE_UPLOAD_PROGRESS, - composeId, - loaded, - total, -}); - -const uploadComposeSuccess = (composeId: string, media: MediaAttachment) => ({ - type: COMPOSE_UPLOAD_SUCCESS, - composeId, - media, -}); - -const uploadComposeFail = (composeId: string, error: unknown) => ({ - type: COMPOSE_UPLOAD_FAIL, - composeId, - error, -}); - -const changeUploadCompose = - (composeId: string, mediaId: string, params: UpdateMediaParams) => - (dispatch: AppDispatch, getState: () => RootState) => { - if (!isLoggedIn(getState)) return Promise.resolve(); - - const compose = getState().compose[composeId]; - - dispatch(changeUploadComposeRequest(composeId)); - - return dispatch(updateMedia(mediaId, params)) - .then((response) => { - dispatch(changeUploadComposeSuccess(composeId, response)); - return response; - }) - .catch((error) => { - if (error.response?.status === 404 && compose.editedId) { - // Editing an existing status. Mastodon doesn't let you update media attachments for already posted statuses. - // Pretend we got a success response. - const previousMedia = compose.mediaAttachments.find((m) => m.id === mediaId); - - if (previousMedia) { - dispatch(changeUploadComposeSuccess(composeId, { ...previousMedia, ...params })); - return; - } - } - dispatch(changeUploadComposeFail(composeId, mediaId, error)); - }); - }; - -const changeUploadComposeRequest = (composeId: string) => ({ - type: COMPOSE_UPLOAD_CHANGE_REQUEST, - composeId, -}); - -const changeUploadComposeSuccess = (composeId: string, media: MediaAttachment) => ({ - type: COMPOSE_UPLOAD_CHANGE_SUCCESS, - composeId, - media, -}); - -const changeUploadComposeFail = (composeId: string, mediaId: string, error: unknown) => ({ - type: COMPOSE_UPLOAD_CHANGE_FAIL, - composeId, - mediaId, - error, -}); - -const undoUploadCompose = (composeId: string, mediaId: string) => ({ - type: COMPOSE_UPLOAD_UNDO, - composeId, - mediaId, -}); - -const groupCompose = (composeId: string, groupId: string) => ({ - type: COMPOSE_GROUP_POST, - composeId, - groupId, -}); - -const clearComposeSuggestions = (composeId: string) => { - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions.abort(); - cancelFetchComposeSuggestions = new AbortController(); - } - return { - type: COMPOSE_SUGGESTIONS_CLEAR, - composeId, - }; -}; - -const fetchComposeSuggestionsAccounts = throttle( - (dispatch, getState, composeId, token) => { - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions.abort(); - cancelFetchComposeSuggestions = new AbortController(); - } - - const signal = cancelFetchComposeSuggestions.signal; - - return getClient(getState) - .accounts.searchAccounts(token.slice(1), { resolve: false, limit: 10 }, { signal }) - .then((response) => { - dispatch(importEntities({ accounts: response })); - dispatch(readyComposeSuggestionsAccounts(composeId, token, response)); - }) - .catch((error) => { - if (!signal.aborted) { - toast.showAlertForError(error); - } - }); - }, - 200, - { leading: true, trailing: true }, -); - -const fetchComposeSuggestionsEmojis = (dispatch: AppDispatch, composeId: string, token: string) => { - const customEmojis = queryClient.getQueryData>(['instance', 'customEmojis']); - const results = emojiSearch(token.replace(':', ''), { maxResults: 10 }, customEmojis); - - dispatch(readyComposeSuggestionsEmojis(composeId, token, results)); -}; - -const fetchComposeSuggestionsTags = ( - dispatch: AppDispatch, - getState: () => RootState, - composeId: string, - token: string, -) => { - const signal = cancelFetchComposeSuggestions.signal; - - if (cancelFetchComposeSuggestions) { - cancelFetchComposeSuggestions.abort(); - cancelFetchComposeSuggestions = new AbortController(); - } - - const state = getState(); - - const { trends } = state.auth.client.features; - - if (trends) { - const currentTrends = queryClient.getQueryData>(['trends', 'tags']) ?? []; - - return dispatch(updateSuggestionTags(composeId, token, currentTrends)); - } - - return getClient(state) - .search.search(token.slice(1), { limit: 10, type: 'hashtags' }, { signal }) - .then((response) => { - dispatch(updateSuggestionTags(composeId, token, response.hashtags)); - }) - .catch((error) => { - if (!signal.aborted) { - toast.showAlertForError(error); - } - }); -}; - -const fetchComposeSuggestions = - (composeId: string, token: string) => (dispatch: AppDispatch, getState: () => RootState) => { - switch (token[0]) { - case ':': - fetchComposeSuggestionsEmojis(dispatch, composeId, token); - break; - case '#': - fetchComposeSuggestionsTags(dispatch, getState, composeId, token); - break; - default: - fetchComposeSuggestionsAccounts(dispatch, getState, composeId, token); - break; - } - }; - -interface ComposeSuggestionsReadyAction { - type: typeof COMPOSE_SUGGESTIONS_READY; - composeId: string; - token: string; - emojis?: Emoji[]; - accounts?: Account[]; -} - -const readyComposeSuggestionsEmojis = (composeId: string, token: string, emojis: Emoji[]) => ({ - type: COMPOSE_SUGGESTIONS_READY, - composeId, - token, - emojis, -}); - -const readyComposeSuggestionsAccounts = ( - composeId: string, - token: string, - accounts: Account[], -) => ({ - type: COMPOSE_SUGGESTIONS_READY, - composeId, - token, - accounts, -}); - -interface ComposeSuggestionSelectAction { - type: typeof COMPOSE_SUGGESTION_SELECT; - composeId: string; - position: number; - token: string | null; - completion: string; - path: ['spoiler_text'] | ['poll', 'options', number]; -} - -const selectComposeSuggestion = - ( - composeId: string, - position: number, - token: string | null, - suggestion: AutoSuggestion, - path: ComposeSuggestionSelectAction['path'], - ) => - (dispatch: AppDispatch, getState: () => RootState) => { - let completion = '', - startPosition = position; - - if (typeof suggestion === 'object' && 'id' in suggestion) { - completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; - startPosition = position - 1; - - useSettingsStore.getState().actions.rememberEmojiUse(suggestion); - dispatch(saveSettings()); - } else if (typeof suggestion === 'string' && suggestion[0] === '#') { - completion = suggestion; - startPosition = position - 1; - } else if (typeof suggestion === 'string') { - completion = selectAccount(suggestion)!.acct; - startPosition = position; - } - - dispatch({ - type: COMPOSE_SUGGESTION_SELECT, - composeId, - position: startPosition, - token, - completion, - path, - }); - }; - -const updateSuggestionTags = (composeId: string, token: string, tags: Array) => ({ - type: COMPOSE_SUGGESTION_TAGS_UPDATE, - composeId, - token, - tags, -}); - -const changeComposeSpoilerness = (composeId: string) => ({ - type: COMPOSE_SPOILERNESS_CHANGE, - composeId, -}); - -const changeComposeContentType = (composeId: string, value: string) => ({ - type: COMPOSE_TYPE_CHANGE, - composeId, - value, -}); - -const changeComposeSpoilerText = (composeId: string, text: string) => ({ - type: COMPOSE_SPOILER_TEXT_CHANGE, - composeId, - text, -}); - -const changeComposeVisibility = (composeId: string, value: string) => ({ - type: COMPOSE_VISIBILITY_CHANGE, - composeId, - value, -}); - -const changeComposeLanguage = (composeId: string, value: Language | null) => ({ - type: COMPOSE_LANGUAGE_CHANGE, - composeId, - value, -}); - -const changeComposeModifiedLanguage = (composeId: string, value: Language | null) => ({ - type: COMPOSE_MODIFIED_LANGUAGE_CHANGE, - composeId, - value, -}); - -const addComposeLanguage = (composeId: string, value: Language) => ({ - type: COMPOSE_LANGUAGE_ADD, - composeId, - value, -}); - -const deleteComposeLanguage = (composeId: string, value: Language) => ({ - type: COMPOSE_LANGUAGE_DELETE, - composeId, - value, -}); - -const addPoll = (composeId: string) => ({ - type: COMPOSE_POLL_ADD, - composeId, -}); - -const removePoll = (composeId: string) => ({ - type: COMPOSE_POLL_REMOVE, - composeId, -}); - -const addSchedule = (composeId: string) => ({ - type: COMPOSE_SCHEDULE_ADD, - composeId, -}); - -const setSchedule = (composeId: string, date: Date) => ({ - type: COMPOSE_SCHEDULE_SET, - composeId, - date: date, -}); - -const removeSchedule = (composeId: string) => ({ - type: COMPOSE_SCHEDULE_REMOVE, - composeId, -}); - -const addPollOption = (composeId: string, title: string) => ({ - type: COMPOSE_POLL_OPTION_ADD, - composeId, - title, -}); - -const changePollOption = (composeId: string, index: number, title: string) => ({ - type: COMPOSE_POLL_OPTION_CHANGE, - composeId, - index, - title, -}); - -const removePollOption = (composeId: string, index: number) => ({ - type: COMPOSE_POLL_OPTION_REMOVE, - composeId, - index, -}); - -const changePollSettings = (composeId: string, expiresIn?: number, isMultiple?: boolean) => ({ - type: COMPOSE_POLL_SETTINGS_CHANGE, - composeId, - expiresIn, - isMultiple, -}); - -const openComposeWithText = - (composeId: string, text = '') => - (dispatch: AppDispatch) => { - dispatch(resetCompose(composeId)); - useModalsStore.getState().actions.openModal('COMPOSE'); - dispatch(changeCompose(composeId, text)); - }; - -interface ComposeAddToMentionsAction { - type: typeof COMPOSE_ADD_TO_MENTIONS; - composeId: string; - account: string; -} - -const addToMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch) => { - const account = selectAccount(accountId); - if (!account) return; - - return dispatch({ - type: COMPOSE_ADD_TO_MENTIONS, - composeId, - account: account.acct, - }); -}; - -interface ComposeRemoveFromMentionsAction { - type: typeof COMPOSE_REMOVE_FROM_MENTIONS; - composeId: string; - account: string; -} - -const removeFromMentions = (composeId: string, accountId: string) => (dispatch: AppDispatch) => { - const account = selectAccount(accountId); - if (!account) return; - - return dispatch({ - type: COMPOSE_REMOVE_FROM_MENTIONS, - composeId, - account: account.acct, - }); -}; - -interface ComposeEventReplyAction { - type: typeof COMPOSE_EVENT_REPLY; - composeId: string; - status: Pick; - account: Pick; - explicitAddressing: boolean; -} - -const eventDiscussionCompose = - (composeId: string, status: ComposeEventReplyAction['status']) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const { forceImplicitAddressing } = useSettingsStore.getState().settings; - const explicitAddressing = - state.auth.client.features.createStatusExplicitAddressing && !forceImplicitAddressing; - - return dispatch({ - type: COMPOSE_EVENT_REPLY, - composeId, - status, - account: selectOwnAccount(state), - explicitAddressing, - }); - }; - -const setEditorState = ( - composeId: string, - editorState: EditorState | string | null, - text?: string, -) => ({ - type: COMPOSE_EDITOR_STATE_SET, - composeId, - editorState, - text, -}); - -const changeMediaOrder = (composeId: string, a: string, b: string) => ({ - type: COMPOSE_CHANGE_MEDIA_ORDER, - composeId, - a, - b, -}); - -const addSuggestedQuote = (composeId: string, quoteId: string) => ({ - type: COMPOSE_ADD_SUGGESTED_QUOTE, - composeId, - quoteId, -}); - -const addSuggestedLanguage = (composeId: string, language: string) => ({ - type: COMPOSE_ADD_SUGGESTED_LANGUAGE, - composeId, - language, -}); - -const changeComposeFederated = (composeId: string) => ({ - type: COMPOSE_FEDERATED_CHANGE, - composeId, -}); - -const changeComposeInteractionPolicyOption = ( - composeId: string, - policy: Policy, - rule: Rule, - value: Scope[], - initial: InteractionPolicy, -) => ({ - type: COMPOSE_INTERACTION_POLICY_OPTION_CHANGE, - composeId, - policy, - rule, - value, - initial, -}); - -const changeComposeQuotePolicyOption = ( - composeId: string, - value: CreateStatusParams['quote_approval_policy'], -) => ({ - type: COMPOSE_QUOTE_POLICY_OPTION_CHANGE, - composeId, - value, -}); - -const suggestClearLink = (composeId: string, suggestion: ClearLinkSuggestion | null) => ({ - type: COMPOSE_CLEAR_LINK_SUGGESTION_CREATE, - composeId, - suggestion, -}); - -const ignoreClearLinkSuggestion = (composeId: string, key: string) => ({ - type: COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE, - composeId, - key, -}); - -const suggestHashtagCasing = (composeId: string, suggestion: string | null) => ({ - type: COMPOSE_HASHTAG_CASING_SUGGESTION_SET, - composeId, - suggestion, -}); - -const ignoreHashtagCasingSuggestion = (composeId: string) => ({ - type: COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE, - composeId, -}); - -const changeComposeRedactingOverwrite = (composeId: string, value: boolean) => ({ - type: COMPOSE_REDACTING_OVERWRITE_CHANGE, - composeId, - value, -}); - -const setComposeLocation = (composeId: string, location: Location | null) => ({ - type: COMPOSE_SET_LOCATION, - composeId, - location, -}); - -const setComposeShowLocationPicker = (composeId: string, showLocation: boolean) => ({ - type: COMPOSE_SET_SHOW_LOCATION_PICKER, - composeId, - showLocation, -}); - -type ComposeAction = - | ComposeSetStatusAction - | ReturnType - | ComposeReplyAction - | ReturnType - | ComposeQuoteAction - | ReturnType - | ReturnType - | ComposeMentionAction - | ComposeDirectAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ComposeSuggestionsReadyAction - | ComposeSuggestionSelectAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ComposeAddToMentionsAction - | ComposeRemoveFromMentionsAction - | ComposeEventReplyAction - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType - | ReturnType; - -export { - COMPOSE_CHANGE, - COMPOSE_SUBMIT_REQUEST, - COMPOSE_SUBMIT_SUCCESS, - COMPOSE_SUBMIT_FAIL, - COMPOSE_PREVIEW_SUCCESS, - COMPOSE_PREVIEW_CANCEL, - COMPOSE_REPLY, - COMPOSE_REPLY_CANCEL, - COMPOSE_EVENT_REPLY, - COMPOSE_QUOTE, - COMPOSE_QUOTE_CANCEL, - COMPOSE_DIRECT, - COMPOSE_MENTION, - COMPOSE_RESET, - COMPOSE_UPLOAD_REQUEST, - COMPOSE_UPLOAD_SUCCESS, - COMPOSE_UPLOAD_FAIL, - COMPOSE_UPLOAD_PROGRESS, - COMPOSE_UPLOAD_UNDO, - COMPOSE_GROUP_POST, - COMPOSE_SUGGESTIONS_CLEAR, - COMPOSE_SUGGESTIONS_READY, - COMPOSE_SUGGESTION_SELECT, - COMPOSE_SUGGESTION_TAGS_UPDATE, - COMPOSE_SPOILERNESS_CHANGE, - COMPOSE_TYPE_CHANGE, - COMPOSE_SPOILER_TEXT_CHANGE, - COMPOSE_VISIBILITY_CHANGE, - COMPOSE_LANGUAGE_CHANGE, - COMPOSE_MODIFIED_LANGUAGE_CHANGE, - COMPOSE_LANGUAGE_ADD, - COMPOSE_LANGUAGE_DELETE, - COMPOSE_UPLOAD_CHANGE_REQUEST, - COMPOSE_UPLOAD_CHANGE_SUCCESS, - COMPOSE_UPLOAD_CHANGE_FAIL, - COMPOSE_POLL_ADD, - COMPOSE_POLL_REMOVE, - COMPOSE_POLL_OPTION_ADD, - COMPOSE_POLL_OPTION_CHANGE, - COMPOSE_POLL_OPTION_REMOVE, - COMPOSE_POLL_SETTINGS_CHANGE, - COMPOSE_SCHEDULE_ADD, - COMPOSE_SCHEDULE_SET, - COMPOSE_SCHEDULE_REMOVE, - COMPOSE_ADD_TO_MENTIONS, - COMPOSE_REMOVE_FROM_MENTIONS, - COMPOSE_SET_STATUS, - COMPOSE_EDITOR_STATE_SET, - COMPOSE_CHANGE_MEDIA_ORDER, - COMPOSE_ADD_SUGGESTED_QUOTE, - COMPOSE_ADD_SUGGESTED_LANGUAGE, - COMPOSE_FEDERATED_CHANGE, - COMPOSE_INTERACTION_POLICY_OPTION_CHANGE, - COMPOSE_QUOTE_POLICY_OPTION_CHANGE, - COMPOSE_CLEAR_LINK_SUGGESTION_CREATE, - COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE, - COMPOSE_HASHTAG_CASING_SUGGESTION_SET, - COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE, - COMPOSE_REDACTING_OVERWRITE_CHANGE, - COMPOSE_SET_LOCATION, - COMPOSE_SET_SHOW_LOCATION_PICKER, - setComposeToStatus, - replyCompose, - cancelReplyCompose, - quoteCompose, - cancelQuoteCompose, - resetCompose, - mentionCompose, - directCompose, - submitCompose, - uploadFile, - uploadCompose, - changeUploadCompose, - uploadComposeSuccess, - undoUploadCompose, - groupCompose, - groupComposeModal, - clearComposeSuggestions, - fetchComposeSuggestions, - selectComposeSuggestion, - changeComposeSpoilerness, - changeComposeContentType, - changeComposeSpoilerText, - changeComposeVisibility, - changeComposeLanguage, - changeComposeModifiedLanguage, - addComposeLanguage, - deleteComposeLanguage, - addPoll, - removePoll, - addSchedule, - setSchedule, - removeSchedule, - addPollOption, - changePollOption, - removePollOption, - changePollSettings, - openComposeWithText, - addToMentions, - removeFromMentions, - eventDiscussionCompose, - setEditorState, - changeMediaOrder, - addSuggestedQuote, - addSuggestedLanguage, - changeComposeFederated, - changeComposeInteractionPolicyOption, - changeComposeQuotePolicyOption, - suggestClearLink, - ignoreClearLinkSuggestion, - cancelPreviewCompose, - suggestHashtagCasing, - ignoreHashtagCasingSuggestion, - changeComposeRedactingOverwrite, - setComposeLocation, - setComposeShowLocationPicker, - type ComposeReplyAction, - type ComposeSuggestionSelectAction, - type ComposeAction, -}; diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index 103deffb5..eadb820a1 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -1,6 +1,7 @@ import { defineMessages } from 'react-intl'; import { getClient } from '@/api'; +import { useComposeStore } from '@/stores/compose'; import toast from '@/toast'; import { importEntities } from './importer'; @@ -19,10 +20,6 @@ const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL' as const; const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST' as const; const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL' as const; -const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const; - -const EVENT_FORM_SET = 'EVENT_FORM_SET' as const; - const messages = defineMessages({ exceededImageSizeLimit: { id: 'upload_error.image_size_limit', @@ -123,15 +120,12 @@ interface LeaveEventFail { const fetchEventIcs = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => getClient(getState).events.getEventIcs(statusId); -const cancelEventCompose = () => ({ - type: EVENT_COMPOSE_CANCEL, -}); - -interface EventFormSetAction { - type: typeof EVENT_FORM_SET; - composeId: string; - text: string; -} +// todo: move to compose store? +const cancelEventCompose = () => { + useComposeStore.getState().actions.updateCompose('event-compose-modal', (draft) => { + draft.text = ''; + }); +}; const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ type: STATUS_FETCH_SOURCE_REQUEST, statusId }); @@ -140,11 +134,11 @@ const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () .statuses.getStatusSource(statusId) .then((response) => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS, statusId }); - dispatch({ - type: EVENT_FORM_SET, - composeId: `compose-event-modal-${statusId}`, - text: response.text, - }); + useComposeStore + .getState() + .actions.updateCompose(`compose-event-modal-${statusId}`, (draft) => { + draft.text = response.text; + }); return response; }) .catch((error) => { @@ -152,21 +146,13 @@ const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: () }); }; -type EventsAction = - | JoinEventRequest - | JoinEventFail - | LeaveEventRequest - | LeaveEventFail - | ReturnType - | EventFormSetAction; +type EventsAction = JoinEventRequest | JoinEventFail | LeaveEventRequest | LeaveEventFail; export { EVENT_JOIN_REQUEST, EVENT_JOIN_FAIL, EVENT_LEAVE_REQUEST, EVENT_LEAVE_FAIL, - EVENT_COMPOSE_CANCEL, - EVENT_FORM_SET, submitEvent, fetchEventIcs, cancelEventCompose, diff --git a/packages/pl-fe/src/actions/instance.ts b/packages/pl-fe/src/actions/instance.ts index 6e934c53c..e90e4ba17 100644 --- a/packages/pl-fe/src/actions/instance.ts +++ b/packages/pl-fe/src/actions/instance.ts @@ -1,3 +1,4 @@ +import { useComposeStore } from '@/stores/compose'; import { getAuthUserUrl, getMeUrl } from '@/utils/auth'; import { getClient, staticFetch } from '../api'; @@ -35,6 +36,7 @@ const fetchInstance = () => async (dispatch: AppDispatch, getState: () => RootSt const instance = await getClient(getState).instance.getInstance(); dispatch({ type: INSTANCE_FETCH_SUCCESS, instance }); + useComposeStore.getState().actions.importDefaultContentType(instance); } catch (error) { dispatch({ type: INSTANCE_FETCH_FAIL, error }); } diff --git a/packages/pl-fe/src/actions/me.ts b/packages/pl-fe/src/actions/me.ts index ee58776f0..27db72148 100644 --- a/packages/pl-fe/src/actions/me.ts +++ b/packages/pl-fe/src/actions/me.ts @@ -1,6 +1,7 @@ import { selectAccount } from '@/queries/accounts/selectors'; import { setSentryAccount } from '@/sentry'; import KVStore from '@/storage/kv-store'; +import { useComposeStore } from '@/stores/compose'; import { useSettingsStore } from '@/stores/settings'; import { getAuthUserId, getAuthUserUrl } from '@/utils/auth'; @@ -89,6 +90,7 @@ const fetchMeSuccess = (account: CredentialAccount) => { setSentryAccount(account); useSettingsStore.getState().actions.loadUserSettings(account.settings_store?.[FE_NAME]); + useComposeStore.getState().actions.importDefaultSettings(account); return { type: ME_FETCH_SUCCESS, @@ -109,6 +111,7 @@ interface MePatchSuccessAction { const patchMeSuccess = (me: CredentialAccount) => (dispatch: AppDispatch) => { dispatch(importEntities({ accounts: [me] })); + useComposeStore.getState().actions.importDefaultSettings(me); dispatch({ type: ME_PATCH_SUCCESS, me, diff --git a/packages/pl-fe/src/actions/statuses.ts b/packages/pl-fe/src/actions/statuses.ts index 6e42df438..7714877ea 100644 --- a/packages/pl-fe/src/actions/statuses.ts +++ b/packages/pl-fe/src/actions/statuses.ts @@ -1,5 +1,6 @@ import { queryClient } from '@/queries/client'; import { scheduledStatusesQueryOptions } from '@/queries/statuses/scheduled-statuses'; +import { useComposeStore } from '@/stores/compose'; import { useContextStore } from '@/stores/contexts'; import { useModalsStore } from '@/stores/modals'; import { usePendingStatusesStore } from '@/stores/pending-statuses'; @@ -9,19 +10,12 @@ import { shouldHaveCard } from '@/utils/status'; import { getClient } from '../api'; -import { setComposeToStatus } from './compose'; import { importEntities } from './importer'; import { deleteFromTimelines } from './timelines'; import type { Status } from '@/normalizers/status'; import type { AppDispatch, RootState } from '@/store'; -import type { - CreateStatusParams, - Status as BaseStatus, - ScheduledStatus, - StatusSource, - Poll, -} from 'pl-api'; +import type { CreateStatusParams, Status as BaseStatus, ScheduledStatus, Poll } from 'pl-api'; import type { IntlShape } from 'react-intl'; const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST' as const; @@ -157,16 +151,7 @@ const editStatus = (statusId: string) => (dispatch: AppDispatch, getState: () => .statuses.getStatusSource(statusId) .then((response) => { dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS }); - dispatch( - setComposeToStatus( - status, - poll, - response.text, - response.spoiler_text, - response.content_type, - false, - ), - ); + useComposeStore.getState().actions.setComposeToStatus(status, poll, response); useModalsStore.getState().actions.openModal('COMPOSE'); }) .catch((error) => { @@ -192,7 +177,7 @@ const fetchStatus = }; const deleteStatus = - (statusId: string, groupId?: string, withRedraft = false) => + (statusId: string, withRedraft = false) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return null; @@ -205,27 +190,15 @@ const deleteStatus = dispatch({ type: STATUS_DELETE_REQUEST, params: status }); - return ( - groupId - ? getClient(state).experimental.groups.deleteGroupStatus(statusId, groupId) - : getClient(state).statuses.deleteStatus(statusId) - ) - .then((response) => { + return getClient(state) + .statuses.deleteStatus(statusId) + .then((source) => { usePendingStatusesStore.getState().actions.deleteStatus(statusId); dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); dispatch(deleteFromTimelines(statusId)); if (withRedraft) { - dispatch( - setComposeToStatus( - status, - poll, - response.text ?? '', - response.spoiler_text, - (response as StatusSource).content_type, - withRedraft, - ), - ); + useComposeStore.getState().actions.setComposeToStatus(status, poll, source, withRedraft); useModalsStore.getState().actions.openModal('COMPOSE'); } }) @@ -234,6 +207,27 @@ const deleteStatus = }); }; +const deleteStatusFromGroup = + (statusId: string, groupId: string) => (dispatch: AppDispatch, getState: () => RootState) => { + if (!isLoggedIn(getState)) return null; + + const state = getState(); + const status = state.statuses[statusId]; + + dispatch({ type: STATUS_DELETE_REQUEST, params: status }); + + return getClient(state) + .experimental.groups.deleteGroupStatus(statusId, groupId) + .then((response) => { + usePendingStatusesStore.getState().actions.deleteStatus(statusId); + dispatch({ type: STATUS_DELETE_SUCCESS, statusId }); + dispatch(deleteFromTimelines(statusId)); + }) + .catch((error) => { + dispatch({ type: STATUS_DELETE_FAIL, params: status, error }); + }); + }; + const updateStatus = (status: BaseStatus) => (dispatch: AppDispatch) => { dispatch(importEntities({ statuses: [status] })); }; @@ -404,6 +398,7 @@ export { editStatus, fetchStatus, deleteStatus, + deleteStatusFromGroup, updateStatus, fetchContext, fetchStatusWithContext, diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 04de2ced3..fd9eff940 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -1,4 +1,5 @@ import { getLocale } from '@/actions/settings'; +import { useComposeStore } from '@/stores/compose'; import { useContextStore } from '@/stores/contexts'; import { usePendingStatusesStore } from '@/stores/pending-statuses'; import { useSettingsStore } from '@/stores/settings'; @@ -133,6 +134,7 @@ const deleteFromTimelines = const reblogOf = getState().statuses[statusId]?.reblog_id ?? null; useContextStore.getState().actions.deleteStatuses([statusId]); + useComposeStore.getState().actions.handleTimelineDelete(statusId); dispatch({ type: TIMELINE_DELETE, diff --git a/packages/pl-fe/src/components/autosuggest-input.tsx b/packages/pl-fe/src/components/autosuggest-input.tsx index 929532bce..9fff5994f 100644 --- a/packages/pl-fe/src/components/autosuggest-input.tsx +++ b/packages/pl-fe/src/components/autosuggest-input.tsx @@ -300,4 +300,4 @@ const AutosuggestInput: React.FC = ({ ]; }; -export { type AutoSuggestion, type IAutosuggestInput, AutosuggestInput as default }; +export { type AutoSuggestion, AutosuggestInput as default }; diff --git a/packages/pl-fe/src/components/modal-root.tsx b/packages/pl-fe/src/components/modal-root.tsx index b65d2262b..f022ab1f0 100644 --- a/packages/pl-fe/src/components/modal-root.tsx +++ b/packages/pl-fe/src/components/modal-root.tsx @@ -4,14 +4,13 @@ import range from 'lodash/range'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { cancelReplyCompose } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { usePrevious } from '@/hooks/use-previous'; import { usePersistDraftStatus } from '@/queries/statuses/use-draft-statuses'; +import { useComposeStore } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import type { ModalType } from '@/features/ui/components/modal-root'; -import type { Compose } from '@/reducers/compose'; +import type { Compose } from '@/stores/compose'; const messages = defineMessages({ confirm: { id: 'confirmations.cancel.confirm', defaultMessage: 'Discard' }, @@ -40,8 +39,6 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo const intl = useIntl(); const router = useRouter(); const navigate = useNavigate(); - const dispatch = useAppDispatch(); - const persistDraftStatus = usePersistDraftStatus(); const { openModal } = useModalsActions(); @@ -64,65 +61,64 @@ const ModalRoot: React.FC = ({ children, onCancel, onClose, type, mo }; const handleOnClose = () => { - dispatch((_, getState) => { - const compose = getState().compose['compose-modal']; - const hasComposeContent = checkComposeContent(compose); + const { actions } = useComposeStore.getState(); + const compose = actions.getCompose('compose-modal'); + const hasComposeContent = checkComposeContent(compose); - if (hasComposeContent && type === 'COMPOSE') { - const isEditing = compose.editedId !== null; - openModal('CONFIRM', { - heading: isEditing ? ( - - ) : compose.draftId ? ( - - ) : ( - - ), - message: isEditing ? ( - - ) : compose.draftId ? ( - - ) : ( - - ), - confirm: intl.formatMessage(messages.confirm), - onConfirm: () => { - onClose('COMPOSE'); - dispatch(cancelReplyCompose()); - }, - onCancel: () => { - onClose('CONFIRM'); - }, - secondary: intl.formatMessage(messages.saveDraft), - onSecondary: isEditing - ? undefined - : () => { - persistDraftStatus('compose-modal'); - onClose('COMPOSE'); - dispatch(cancelReplyCompose()); - }, - }); - } else if (hasComposeContent && type === 'CONFIRM') { - onClose('CONFIRM'); - } else { - onClose(); - } - }); + if (hasComposeContent && type === 'COMPOSE') { + const isEditing = compose.editedId !== null; + openModal('CONFIRM', { + heading: isEditing ? ( + + ) : compose.draftId ? ( + + ) : ( + + ), + message: isEditing ? ( + + ) : compose.draftId ? ( + + ) : ( + + ), + confirm: intl.formatMessage(messages.confirm), + onConfirm: () => { + onClose('COMPOSE'); + actions.resetCompose('compose-modal'); + }, + onCancel: () => { + onClose('CONFIRM'); + }, + secondary: intl.formatMessage(messages.saveDraft), + onSecondary: isEditing + ? undefined + : () => { + persistDraftStatus('compose-modal'); + onClose('COMPOSE'); + actions.resetCompose('compose-modal'); + }, + }); + } else if (hasComposeContent && type === 'CONFIRM') { + onClose('CONFIRM'); + } else { + onClose(); + } }; const handleKeyDown = useCallback((e: KeyboardEvent) => { diff --git a/packages/pl-fe/src/components/status-action-bar.tsx b/packages/pl-fe/src/components/status-action-bar.tsx index 753a71589..c26514d1b 100644 --- a/packages/pl-fe/src/components/status-action-bar.tsx +++ b/packages/pl-fe/src/components/status-action-bar.tsx @@ -4,12 +4,16 @@ import React, { useCallback, useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { redactStatus } from '@/actions/admin'; -import { directCompose, mentionCompose, quoteCompose, replyCompose } from '@/actions/compose'; import { emojiReact, unEmojiReact } from '@/actions/emoji-reacts'; import { deleteStatusModal, toggleStatusSensitivityModal } from '@/actions/moderation'; import { initReport, ReportableEntities } from '@/actions/reports'; import { changeSetting } from '@/actions/settings'; -import { deleteStatus, editStatus, toggleMuteStatus } from '@/actions/statuses'; +import { + deleteStatus, + deleteStatusFromGroup, + editStatus, + toggleMuteStatus, +} from '@/actions/statuses'; import DropdownMenu from '@/components/dropdown-menu'; import StatusActionButton from '@/components/status-action-button'; import EmojiPickerDropdown from '@/features/emoji/containers/emoji-picker-dropdown-container'; @@ -40,6 +44,7 @@ import { useUnpinStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import { useStatusMeta, useStatusMetaActions } from '@/stores/status-meta'; @@ -332,7 +337,7 @@ const ReplyButton: React.FC = ({ onOpenUnauthorizedModal, rebloggedBy, }) => { - const dispatch = useAppDispatch(); + const { replyCompose } = useComposeActions(); const intl = useIntl(); const canReply = useCanInteract(status, 'can_reply'); @@ -354,7 +359,7 @@ const ReplyButton: React.FC = ({ const handleReplyClick: React.MouseEventHandler = (e) => { if (me) { - dispatch(replyCompose(status, rebloggedBy, canReply.approvalRequired ?? false)); + replyCompose(status, rebloggedBy, canReply.approvalRequired ?? false); } else { onOpenUnauthorizedModal('REPLY'); } @@ -405,7 +410,7 @@ const ReblogButton: React.FC = ({ onOpenUnauthorizedModal, publicStatus, }) => { - const dispatch = useAppDispatch(); + const { quoteCompose } = useComposeActions(); const features = useFeatures(); const intl = useIntl(); @@ -486,7 +491,7 @@ const ReblogButton: React.FC = ({ const handleQuoteClick: React.EventHandler = (e) => { if (me) { - dispatch(quoteCompose(status, canQuote.approvalRequired || false)); + quoteCompose(status, canQuote.approvalRequired || false); } else { onOpenUnauthorizedModal('REBLOG'); } @@ -723,6 +728,7 @@ const MenuButton: React.FC = ({ const intl = useIntl(); const navigate = useNavigate(); const dispatch = useAppDispatch(); + const { mentionCompose, directCompose } = useComposeActions(); const match = useMatch({ from: layouts.group.id, shouldThrow: false }); const { boostModal } = useSettings(); const client = useClient(); @@ -788,7 +794,7 @@ const MenuButton: React.FC = ({ const doDeleteStatus = (withRedraft = false) => { if (!deleteModal) { - dispatch(deleteStatus(status.id, undefined, withRedraft)); + dispatch(deleteStatus(status.id, withRedraft)); } else { openModal('CONFIRM', { heading: intl.formatMessage( @@ -800,7 +806,7 @@ const MenuButton: React.FC = ({ confirm: intl.formatMessage( withRedraft ? messages.redraftConfirm : messages.deleteConfirm, ), - onConfirm: () => dispatch(deleteStatus(status.id, undefined, withRedraft)), + onConfirm: () => dispatch(deleteStatus(status.id, withRedraft)), }); } }; @@ -840,11 +846,11 @@ const MenuButton: React.FC = ({ }; const handleMentionClick: React.EventHandler = (e) => { - dispatch(mentionCompose(status.account)); + mentionCompose(status.account); }; const handleDirectClick: React.EventHandler = (e) => { - dispatch(directCompose(status.account)); + directCompose(status.account); }; const handleChatClick: React.EventHandler = (e) => { @@ -936,7 +942,7 @@ const MenuButton: React.FC = ({ }), confirm: intl.formatMessage(messages.deleteConfirm), onConfirm: () => { - dispatch(deleteStatus(status.id, group?.id)); + dispatch(deleteStatusFromGroup(status.id, group!.id)); }, }); }; diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index 0cef982c2..47e514140 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -3,7 +3,6 @@ import clsx from 'clsx'; import React, { useEffect, useMemo, useRef } from 'react'; import { defineMessages, useIntl, FormattedList, FormattedMessage } from 'react-intl'; -import { mentionCompose, replyCompose } from '@/actions/compose'; import { unfilterStatus } from '@/actions/statuses'; import Card from '@/components/ui/card'; import Icon from '@/components/ui/icon'; @@ -23,6 +22,7 @@ import { useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; import { makeGetStatus, type SelectedStatus } from '@/selectors'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import { useStatusMetaActions } from '@/stores/status-meta'; @@ -197,6 +197,7 @@ const Status: React.FC = (props) => { const { toggleStatusesMediaHidden } = useStatusMetaActions(); const { openModal } = useModalsActions(); + const { replyCompose, mentionCompose } = useComposeActions(); const { boostModal } = useSettings(); const didShowCard = useRef(false); const node = useRef(null); @@ -276,7 +277,7 @@ const Status: React.FC = (props) => { if (status.rss_feed) return; e?.preventDefault(); - dispatch(replyCompose(actualStatus, status.reblog_id ? status.account : undefined)); + replyCompose(actualStatus, status.reblog_id ? status.account : undefined); }; const handleHotkeyFavourite = (e?: KeyboardEvent) => { @@ -305,7 +306,7 @@ const Status: React.FC = (props) => { if (status.rss_feed) return; e?.preventDefault(); - dispatch(mentionCompose(actualStatus.account)); + mentionCompose(actualStatus.account); }; const handleHotkeyOpen = () => { diff --git a/packages/pl-fe/src/components/thumb-navigation.tsx b/packages/pl-fe/src/components/thumb-navigation.tsx index 4c93cd575..86c7b2357 100644 --- a/packages/pl-fe/src/components/thumb-navigation.tsx +++ b/packages/pl-fe/src/components/thumb-navigation.tsx @@ -3,16 +3,15 @@ import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { groupComposeModal } from '@/actions/compose'; import ThumbNavigationLink from '@/components/thumb-navigation-link'; import Icon from '@/components/ui/icon'; import { useStatContext } from '@/contexts/stat-context'; import { layouts } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useFeatures } from '@/hooks/use-features'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useNotificationsUnreadCount } from '@/queries/notifications/use-notifications'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useIsSidebarOpen, useUiStoreActions } from '@/stores/ui'; import { isStandalone } from '@/utils/state'; @@ -31,7 +30,6 @@ const messages = defineMessages({ const ThumbNavigation: React.FC = React.memo((): React.JSX.Element => { const intl = useIntl(); - const dispatch = useAppDispatch(); const { data: account } = useOwnAccount(); const features = useFeatures(); const queryClient = useQueryClient(); @@ -41,6 +39,7 @@ const ThumbNavigation: React.FC = React.memo((): React.JSX.Element => { const isSidebarOpen = useIsSidebarOpen(); const { openSidebar, closeSidebar } = useUiStoreActions(); const { openModal } = useModalsActions(); + const { groupComposeModal } = useComposeActions(); const { unreadChatsCount } = useStatContext(); const standalone = useAppSelector(isStandalone); @@ -48,10 +47,8 @@ const ThumbNavigation: React.FC = React.memo((): React.JSX.Element => { const handleOpenComposeModal = () => { if (match?.params.groupId) { - dispatch((_, getState) => { - const group = queryClient.getQueryData(['groups', match.params.groupId]); - if (group) dispatch(groupComposeModal(group)); - }); + const group = queryClient.getQueryData(['groups', match.params.groupId]); + if (group) groupComposeModal(group); } else { openModal('COMPOSE'); } diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index e7beb3b93..6b3a903b8 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import * as v from 'valibot'; -import { mentionCompose, directCompose } from '@/actions/compose'; import { initReport, ReportableEntities } from '@/actions/reports'; import Account from '@/components/account'; import AltIndicator from '@/components/alt-indicator'; @@ -43,6 +42,7 @@ import { blockDomainMutationOptions, unblockDomainMutationOptions, } from '@/queries/settings/domain-blocks'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import toast from '@/toast'; @@ -166,6 +166,7 @@ const Header: React.FC = ({ account }) => { const intl = useIntl(); const navigate = useNavigate(); const dispatch = useAppDispatch(); + const { mentionCompose, directCompose } = useComposeActions(); const client = useClient(); const features = useFeatures(); @@ -228,11 +229,11 @@ const Header: React.FC = ({ account }) => { }; const onMention = () => { - dispatch(mentionCompose(account)); + mentionCompose(account); }; const onDirect = () => { - dispatch(directCompose(account)); + directCompose(account); }; const onReblogToggle = () => { diff --git a/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx b/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx index a67cfe152..b1e5b3460 100644 --- a/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx +++ b/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx @@ -2,7 +2,6 @@ import { useNavigate } from '@tanstack/react-router'; import React, { useCallback, useEffect, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { changeUploadCompose, resetCompose } from '@/actions/compose'; import { cancelEventCompose, initEventEdit, submitEvent } from '@/actions/events'; import { uploadFile } from '@/actions/media'; import { fetchStatus } from '@/actions/statuses'; @@ -27,6 +26,7 @@ import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; import { makeGetStatus } from '@/selectors'; +import { useChangeUploadCompose, useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -69,6 +69,10 @@ const EditEvent: React.FC = ({ statusId }) => { const navigate = useNavigate(); const { openModal } = useModalsActions(); + const composeId = statusId ? `compose-event-${statusId}` : 'compose-event'; + const { resetCompose } = useComposeActions(); + const changeUploadCompose = useChangeUploadCompose(composeId); + const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector((state) => statusId ? getStatus(state, { id: statusId }) : undefined, @@ -94,8 +98,6 @@ const EditEvent: React.FC = ({ statusId }) => { const [isDisabled, setIsDisabled] = useState(!!statusId); const [isUploading, setIsUploading] = useState(false); - const composeId = statusId ? `compose-event-${statusId}` : 'compose-event'; - const onChangeName: React.ChangeEventHandler = ({ target }) => { setName(target.value); }; @@ -158,14 +160,12 @@ const EditEvent: React.FC = ({ statusId }) => { previousPosition: [0, 0], descriptionLimit: descriptionLimit, onSubmit: (description: string, position: [number, number]) => - dispatch( - changeUploadCompose(composeId, banner.id, { - description, - focus: position - ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` - : undefined, - }), - ).then((media) => setBanner(media || null)), + changeUploadCompose(banner.id, { + description, + focus: position + ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` + : undefined, + }).then((media) => setBanner(media || null)), }); }; @@ -190,7 +190,7 @@ const EditEvent: React.FC = ({ statusId }) => { to: '/@{$username}/events/$statusId', params: { username: status.account.acct, statusId: status.id }, }); - dispatch(resetCompose(composeId)); + resetCompose(composeId); }) .catch(() => {}); }; @@ -218,7 +218,8 @@ const EditEvent: React.FC = ({ statusId }) => { } return () => { - dispatch(cancelEventCompose()); + resetCompose(composeId); + cancelEventCompose(); }; }, [statusId]); diff --git a/packages/pl-fe/src/features/compose/components/compose-form.tsx b/packages/pl-fe/src/features/compose/components/compose-form.tsx index a5be1a385..4d3d95b0d 100644 --- a/packages/pl-fe/src/features/compose/components/compose-form.tsx +++ b/packages/pl-fe/src/features/compose/components/compose-form.tsx @@ -4,17 +4,6 @@ import React, { Suspense, useCallback, useEffect, useRef, useState } from 'react import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { length } from 'stringz'; -import { - submitCompose, - clearComposeSuggestions, - fetchComposeSuggestions, - selectComposeSuggestion, - uploadCompose, - ignoreClearLinkSuggestion, - suggestClearLink, - resetCompose, - changeComposeRedactingOverwrite, -} from '@/actions/compose'; import DropdownMenu from '@/components/dropdown-menu'; import List, { ListItem } from '@/components/list'; import Icon from '@/components/ui/icon'; @@ -22,12 +11,16 @@ import SvgIcon from '@/components/ui/svg-icon'; import Toggle from '@/components/ui/toggle'; import EmojiPickerDropdown from '@/features/emoji/containers/emoji-picker-dropdown-container'; import { ComposeEditor } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; import { usePersistDraftStatus } from '@/queries/statuses/use-draft-statuses'; +import { + useCompose, + useComposeActions, + useUploadCompose, + useSubmitCompose, +} from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -61,7 +54,6 @@ import UploadForm from './upload-form'; import VisualCharacterCounter from './visual-character-counter'; import Warning from './warning'; -import type { AutoSuggestion } from '@/components/autosuggest-input'; import type { Menu } from '@/components/dropdown-menu'; import type { Emoji } from '@/features/emoji'; import type { LinkNode } from '@lexical/link'; @@ -152,11 +144,13 @@ const ComposeForm = ({ compact, }: IComposeForm) => { const intl = useIntl(); - const dispatch = useAppDispatch(); const { configuration } = useInstance(); const { closeModal } = useModalsActions(); + const actions = useComposeActions(); const compose = useCompose(id); + const uploadCompose = useUploadCompose(id); + const submitCompose = useSubmitCompose(id); const maxTootChars = configuration.statuses.max_characters; const features = useFeatures(); const persistDraftStatus = usePersistDraftStatus(); @@ -230,19 +224,17 @@ const ComposeForm = ({ if (!canSubmit) return; e?.preventDefault(); - dispatch( - submitCompose(id, { - onSuccess: () => { - editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); - }, - }), - ); + submitCompose({ + onSuccess: () => { + editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); + }, + }); }; const handlePreview = (e?: React.FormEvent) => { e?.preventDefault(); - dispatch(submitCompose(id, {}, true)); + submitCompose({ preview: true }); }; const handleSaveDraft = (e?: React.FormEvent) => { @@ -250,7 +242,7 @@ const ComposeForm = ({ persistDraftStatus(id); closeModal('COMPOSE'); - dispatch(resetCompose(id)); + actions.resetCompose(id); editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined); toast.success(messages.draftSaved, { @@ -259,22 +251,6 @@ const ComposeForm = ({ }); }; - const onSuggestionsClearRequested = () => { - dispatch(clearComposeSuggestions(id)); - }; - - const onSuggestionsFetchRequested = (token: string | number) => { - dispatch(fetchComposeSuggestions(id, token as string)); - }; - - const onSpoilerSuggestionSelected = ( - tokenStart: number, - token: string | null, - value: AutoSuggestion, - ) => { - dispatch(selectComposeSuggestion(id, tokenStart, token, value, ['spoiler_text'])); - }; - const handleEmojiPick = (data: Emoji) => { const editor = editorRef.current; if (!editor) return; @@ -285,7 +261,7 @@ const ComposeForm = ({ }; const onPaste = (files: FileList) => { - dispatch(uploadCompose(id, files, intl)); + uploadCompose(files); }; const onAcceptClearLinkSuggestion = (key: string) => { @@ -307,16 +283,25 @@ const ComposeForm = ({ textNode.setTextContent(suggestion.cleanUrl); } } - dispatch(suggestClearLink(id, null)); + actions.updateCompose(id, (draft) => { + draft.clearLinkSuggestion = null; + }); }); }; const onRejectClearLinkSuggestion = (key: string) => { - dispatch(ignoreClearLinkSuggestion(id, key)); + actions.updateCompose(id, (draft) => { + if (draft.clearLinkSuggestion?.key === key) { + draft.clearLinkSuggestion = null; + } + draft.dismissedClearLinksSuggestions.push(key); + }); }; const handleChangeRedactingOverwrite: React.ChangeEventHandler = (e) => { - dispatch(changeComposeRedactingOverwrite(id, e.target.checked)); + actions.updateCompose(id, (draft) => { + draft.redactingOverwrite = e.target.checked; + }); }; useEffect(() => { @@ -457,13 +442,7 @@ const ComposeForm = ({ )} {features.spoilers && ( - + )}
diff --git a/packages/pl-fe/src/features/compose/components/content-type-button.tsx b/packages/pl-fe/src/features/compose/components/content-type-button.tsx index 3d5407f4c..ac8fbcd32 100644 --- a/packages/pl-fe/src/features/compose/components/content-type-button.tsx +++ b/packages/pl-fe/src/features/compose/components/content-type-button.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { changeComposeContentType } from '@/actions/compose'; import DropdownMenu from '@/components/dropdown-menu'; import Icon from '@/components/ui/icon'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useInstance } from '@/hooks/use-instance'; +import { useCompose, useComposeActions } from '@/stores/compose'; const messages = defineMessages({ contentTypePlaintext: { @@ -36,13 +34,15 @@ interface IContentTypeButton { const ContentTypeButton: React.FC = ({ composeId, compact }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const instance = useInstance(); const { contentType } = useCompose(composeId); - const handleChange = (contentType: string) => () => - dispatch(changeComposeContentType(composeId, contentType)); + const handleChange = (value: string) => () => + updateCompose(composeId, (draft) => { + draft.contentType = value; + }); const postFormats = instance.pleroma.metadata.post_formats; diff --git a/packages/pl-fe/src/features/compose/components/drive-button.tsx b/packages/pl-fe/src/features/compose/components/drive-button.tsx index 58cfedb9e..6181b9577 100644 --- a/packages/pl-fe/src/features/compose/components/drive-button.tsx +++ b/packages/pl-fe/src/features/compose/components/drive-button.tsx @@ -3,9 +3,8 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; import * as v from 'valibot'; -import { uploadComposeSuccess } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useInstance } from '@/hooks/use-instance'; +import { appendMedia, useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import ComposeFormButton from './compose-form-button'; @@ -20,7 +19,7 @@ interface IDriveButton { const DriveButton: React.FC = ({ composeId }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const { configuration } = useInstance(); const { openModal } = useModalsActions(); @@ -50,7 +49,9 @@ const DriveButton: React.FC = ({ composeId }) => { mime_type: file.content_type, }); - dispatch(uploadComposeSuccess(composeId, mediaAttachment)); + updateCompose(composeId, (draft) => { + appendMedia(draft, mediaAttachment); + }); }, }); }; diff --git a/packages/pl-fe/src/features/compose/components/hashtag-casing-suggestion.tsx b/packages/pl-fe/src/features/compose/components/hashtag-casing-suggestion.tsx index 2fd4a2371..8f538d6db 100644 --- a/packages/pl-fe/src/features/compose/components/hashtag-casing-suggestion.tsx +++ b/packages/pl-fe/src/features/compose/components/hashtag-casing-suggestion.tsx @@ -1,13 +1,12 @@ import React from 'react'; import { defineMessages, FormattedMessage } from 'react-intl'; -import { ignoreHashtagCasingSuggestion } from '@/actions/compose'; import { changeSetting } from '@/actions/settings'; import Button from '@/components/ui/button'; import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; import toast from '@/toast'; import Warning from './warning'; @@ -25,12 +24,16 @@ interface IHashtagCasingSuggestion { const HashtagCasingSuggestion = ({ composeId }: IHashtagCasingSuggestion) => { const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); const suggestion = compose.hashtagCasingSuggestion; const onIgnore = () => { - dispatch(ignoreHashtagCasingSuggestion(composeId)); + updateCompose(composeId, (draft) => { + draft.hashtagCasingSuggestion = null; + draft.hashtagCasingSuggestionIgnored = true; + }); }; const onDontAskAgain = () => { @@ -38,7 +41,7 @@ const HashtagCasingSuggestion = ({ composeId }: IHashtagCasingSuggestion) => { changeSetting(['ignoreHashtagCasingSuggestions'], true, { showAlert: false, save: true }), ); toast.info(messages.hashtagCasingSuggestionsDisabled); - dispatch(ignoreHashtagCasingSuggestion(composeId)); + onIgnore(); }; if (!suggestion) return null; diff --git a/packages/pl-fe/src/features/compose/components/language-dropdown.tsx b/packages/pl-fe/src/features/compose/components/language-dropdown.tsx index ef87d8971..368f5cbe1 100644 --- a/packages/pl-fe/src/features/compose/components/language-dropdown.tsx +++ b/packages/pl-fe/src/features/compose/components/language-dropdown.tsx @@ -3,19 +3,12 @@ import fuzzysort from 'fuzzysort'; import React, { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { - addComposeLanguage, - changeComposeLanguage, - changeComposeModifiedLanguage, - deleteComposeLanguage, -} from '@/actions/compose'; import DropdownMenu from '@/components/dropdown-menu'; import Icon from '@/components/ui/icon'; import Input from '@/components/ui/input'; import { type Language, languages as languagesObject } from '@/features/preferences'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useFeatures } from '@/hooks/use-features'; +import { useCompose, useComposeActions } from '@/stores/compose'; import { useSettings } from '@/stores/settings'; const getFrequentlyUsedLanguages = (languageCounters: Record) => @@ -53,7 +46,7 @@ const getLanguageDropdown = ({ handleClose: handleMenuClose }) => { const intl = useIntl(); const features = useFeatures(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const settings = useSettings(); const frequentlyUsedLanguages = useMemo( () => getFrequentlyUsedLanguages(settings.frequentlyUsedLanguages), @@ -75,9 +68,14 @@ const getLanguageDropdown = if (Object.keys(textMap).length) { if (!(value in textMap || language === value)) return; - dispatch(changeComposeModifiedLanguage(composeId, value)); + updateCompose(composeId, (draft) => { + draft.modifiedLanguage = value; + }); } else { - dispatch(changeComposeLanguage(composeId, value)); + updateCompose(composeId, (draft) => { + draft.language = value; + draft.modifiedLanguage = value; + }); } e.preventDefault(); @@ -93,7 +91,15 @@ const getLanguageDropdown = e.preventDefault(); e.stopPropagation(); - dispatch(addComposeLanguage(composeId, value)); + updateCompose(composeId, (draft) => { + draft.editorStateMap[value] = draft.editorState; + draft.textMap[value] = draft.text; + draft.spoilerTextMap[value] = draft.spoilerText; + if (draft.poll) + draft.poll.options_map.forEach( + (option, key) => (option[value] = draft.poll!.options[key]), + ); + }); }; const handleDeleteLanguageClick: React.EventHandler = (e: MouseEvent | KeyboardEvent) => { @@ -104,7 +110,11 @@ const getLanguageDropdown = e.preventDefault(); e.stopPropagation(); - dispatch(deleteComposeLanguage(composeId, value)); + updateCompose(composeId, (draft) => { + delete draft.editorStateMap[value]; + delete draft.textMap[value]; + delete draft.spoilerTextMap[value]; + }); }; const handleClear: React.MouseEventHandler = (e) => { diff --git a/packages/pl-fe/src/features/compose/components/location-button.tsx b/packages/pl-fe/src/features/compose/components/location-button.tsx index 140e1bd07..15740ca8f 100644 --- a/packages/pl-fe/src/features/compose/components/location-button.tsx +++ b/packages/pl-fe/src/features/compose/components/location-button.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { setComposeShowLocationPicker } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; import ComposeFormButton from './compose-form-button'; @@ -24,7 +22,7 @@ interface ILocationButton { const LocationButton: React.FC = ({ composeId }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); @@ -32,11 +30,12 @@ const LocationButton: React.FC = ({ composeId }) => { const active = compose.showLocationPicker; const onClick = () => { - if (active) { - dispatch(setComposeShowLocationPicker(composeId, false)); - } else { - dispatch(setComposeShowLocationPicker(composeId, true)); - } + updateCompose(composeId, (draft) => { + draft.showLocationPicker = !draft.showLocationPicker; + if (!draft.showLocationPicker) { + draft.location = null; + } + }); }; if (unavailable) { diff --git a/packages/pl-fe/src/features/compose/components/location-form.tsx b/packages/pl-fe/src/features/compose/components/location-form.tsx index 1a374c9bf..58aa6a655 100644 --- a/packages/pl-fe/src/features/compose/components/location-form.tsx +++ b/packages/pl-fe/src/features/compose/components/location-form.tsx @@ -2,7 +2,6 @@ import { Location } from 'pl-api'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { setComposeLocation } from '@/actions/compose'; import { ADDRESS_ICONS } from '@/components/autosuggest-location'; import LocationSearch from '@/components/location-search'; import HStack from '@/components/ui/hstack'; @@ -10,8 +9,7 @@ import Icon from '@/components/ui/icon'; import IconButton from '@/components/ui/icon-button'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; const messages = defineMessages({ resetLocation: { id: 'compose_event.reset_location', defaultMessage: 'Reset location' }, @@ -22,13 +20,15 @@ interface ILocationForm { } const LocationForm: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const intl = useIntl(); const { showLocationPicker, location } = useCompose(composeId); - const onChangeLocation = (location: Location | null) => { - dispatch(setComposeLocation(composeId, location)); + const onChangeLocation = (loc: Location | null) => { + updateCompose(composeId, (draft) => { + draft.location = loc; + }); }; if (!showLocationPicker) { diff --git a/packages/pl-fe/src/features/compose/components/poll-button.tsx b/packages/pl-fe/src/features/compose/components/poll-button.tsx index 54e53262c..8f700518c 100644 --- a/packages/pl-fe/src/features/compose/components/poll-button.tsx +++ b/packages/pl-fe/src/features/compose/components/poll-button.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { addPoll, removePoll } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions, newPoll } from '@/stores/compose'; import ComposeFormButton from './compose-form-button'; @@ -19,7 +17,7 @@ interface IPollButton { const PollButton: React.FC = ({ composeId, disabled }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); @@ -27,11 +25,9 @@ const PollButton: React.FC = ({ composeId, disabled }) => { const active = compose.poll !== null; const onClick = () => { - if (active) { - dispatch(removePoll(composeId)); - } else { - dispatch(addPoll(composeId)); - } + updateCompose(composeId, (draft) => { + draft.poll = active ? null : newPoll(); + }); }; if (unavailable) { diff --git a/packages/pl-fe/src/features/compose/components/polls/poll-form.tsx b/packages/pl-fe/src/features/compose/components/polls/poll-form.tsx index 86a317509..6adb12fea 100644 --- a/packages/pl-fe/src/features/compose/components/polls/poll-form.tsx +++ b/packages/pl-fe/src/features/compose/components/polls/poll-form.tsx @@ -1,16 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { - addPollOption, - changePollOption, - changePollSettings, - clearComposeSuggestions, - fetchComposeSuggestions, - removePoll, - removePollOption, - selectComposeSuggestion, -} from '@/actions/compose'; import AutosuggestInput from '@/components/autosuggest-input'; import Button from '@/components/ui/button'; import Divider from '@/components/ui/divider'; @@ -18,9 +8,9 @@ import HStack from '@/components/ui/hstack'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import Toggle from '@/components/ui/toggle'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useComposeSuggestions } from '@/hooks/use-compose-suggestions'; import { useInstance } from '@/hooks/use-instance'; +import { useCompose, useComposeActions } from '@/stores/compose'; import DurationSelector from './duration-selector'; @@ -69,10 +59,12 @@ const Option: React.FC = ({ onRemovePoll, title, }) => { - const dispatch = useAppDispatch(); + const { selectComposeSuggestion } = useComposeActions(); const intl = useIntl(); - const { suggestions, modifiedLanguage: language } = useCompose(composeId); + const [token, setToken] = useState(''); + const suggestions = useComposeSuggestions(token); + const { modifiedLanguage: language } = useCompose(composeId); const handleOptionTitleChange = (event: React.ChangeEvent) => { onChange(index, event.target.value); @@ -86,10 +78,10 @@ const Option: React.FC = ({ } }; - const onSuggestionsClearRequested = () => dispatch(clearComposeSuggestions(composeId)); + const onSuggestionsClearRequested = () => setToken(''); const onSuggestionsFetchRequested = (token: string) => { - dispatch(fetchComposeSuggestions(composeId, token)); + setToken(token); }; const onSuggestionSelected = ( @@ -98,9 +90,7 @@ const Option: React.FC = ({ value: AutoSuggestion, ) => { if (token && typeof token === 'string') { - dispatch( - selectComposeSuggestion(composeId, tokenStart, token, value, ['poll', 'options', index]), - ); + selectComposeSuggestion(composeId, tokenStart, token, value, ['poll', 'options', index]); } }; @@ -146,7 +136,7 @@ interface IPollForm { } const PollForm: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const intl = useIntl(); const { configuration } = useInstance(); @@ -162,15 +152,40 @@ const PollForm: React.FC = ({ composeId }) => { const { max_options: maxOptions, max_characters_per_option: maxOptionChars } = configuration.polls; - const onRemoveOption = (index: number) => dispatch(removePollOption(composeId, index)); + const onRemoveOption = (index: number) => + updateCompose(composeId, (draft) => { + if (!draft.poll) return; + draft.poll.options = draft.poll.options.filter((_, i) => i !== index); + draft.poll.options_map = draft.poll.options_map.filter((_, i) => i !== index); + }); const onChangeOption = (index: number, title: string) => - dispatch(changePollOption(composeId, index, title)); - const handleAddOption = () => dispatch(addPollOption(composeId, '')); - const onChangeSettings = (expiresIn: number, isMultiple?: boolean) => - dispatch(changePollSettings(composeId, expiresIn, isMultiple)); - const handleSelectDuration = (value: number) => onChangeSettings(value, isMultiple); - const handleToggleMultiple = () => onChangeSettings(Number(expiresIn), !isMultiple); - const onRemovePoll = () => dispatch(removePoll(composeId)); + updateCompose(composeId, (draft) => { + if (!draft.poll) return; + if (!draft.modifiedLanguage || draft.modifiedLanguage === draft.language) { + draft.poll.options[index] = title; + if (draft.modifiedLanguage) draft.poll.options_map[index][draft.modifiedLanguage] = title; + } + }); + const handleAddOption = () => + updateCompose(composeId, (draft) => { + if (!draft.poll) return; + draft.poll.options.push(''); + draft.poll.options_map.push( + Object.fromEntries(Object.entries(draft.textMap).map((key) => [key, ''])), + ); + }); + const handleSelectDuration = (value: number) => + updateCompose(composeId, (draft) => { + if (draft.poll) draft.poll.expires_in = value; + }); + const handleToggleMultiple = () => + updateCompose(composeId, (draft) => { + if (draft.poll) draft.poll.multiple = !draft.poll.multiple; + }); + const onRemovePoll = () => + updateCompose(composeId, (draft) => { + draft.poll = null; + }); if (!options) { return null; diff --git a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx index 1545d118c..2c0b3afa1 100644 --- a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx +++ b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx @@ -1,15 +1,13 @@ import React, { useMemo } from 'react'; import { useIntl, defineMessages, IntlShape } from 'react-intl'; -import { changeComposeFederated, changeComposeVisibility } from '@/actions/compose'; import DropdownMenu, { MenuItem } from '@/components/dropdown-menu'; import Icon from '@/components/ui/icon'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useFeatures } from '@/hooks/use-features'; import { getOrderedLists } from '@/pages/account-lists/lists'; import { useCircles } from '@/queries/accounts/use-circles'; import { useLists } from '@/queries/accounts/use-lists'; +import { useCompose, useComposeActions } from '@/stores/compose'; import type { Circle, Features } from 'pl-api'; @@ -156,7 +154,7 @@ interface IPrivacyDropdown { const PrivacyDropdown: React.FC = ({ composeId, compact }) => { const intl = useIntl(); const features = useFeatures(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); const { data: lists = [] } = useLists(getOrderedLists); @@ -167,7 +165,11 @@ const PrivacyDropdown: React.FC = ({ composeId, compact }) => const value = compose.visibility; const unavailable = !!compose.editedId; - const onChange = (value: string) => value && dispatch(changeComposeVisibility(composeId, value)); + const onChange = (value: string) => + value && + updateCompose(composeId, (draft) => { + draft.visibility = value; + }); const options = useMemo( () => getItems(features, lists, circles, isReply, intl), @@ -191,7 +193,10 @@ const PrivacyDropdown: React.FC = ({ composeId, compact }) => meta: intl.formatMessage(messages.localLong), type: 'toggle', checked: compose.localOnly, - onChange: () => dispatch(changeComposeFederated(composeId)), + onChange: () => + updateCompose(composeId, (draft) => { + draft.localOnly = !draft.localOnly; + }), }); const valueOption = useMemo( diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index fec71c11a..e14d2487b 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -7,6 +7,7 @@ import Emojify from '@/features/emoji/emojify'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useGroupQuery } from '@/queries/groups/use-group'; import { makeGetStatus } from '@/selectors'; +import { useCompose } from '@/stores/compose'; interface IReplyGroupIndicator { composeId: string; @@ -16,10 +17,9 @@ const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { const { composeId } = props; const getStatus = useCallback(makeGetStatus(), []); + const { inReplyToId } = useCompose(composeId); - const status = useAppSelector((state) => - getStatus(state, { id: state.compose[composeId]?.inReplyToId! }), - ); + const status = useAppSelector((state) => getStatus(state, { id: inReplyToId! })); const { data: group } = useGroupQuery(status?.group_id ?? undefined); diff --git a/packages/pl-fe/src/features/compose/components/schedule-button.tsx b/packages/pl-fe/src/features/compose/components/schedule-button.tsx index 606e0fb11..978fb538e 100644 --- a/packages/pl-fe/src/features/compose/components/schedule-button.tsx +++ b/packages/pl-fe/src/features/compose/components/schedule-button.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { addSchedule, removeSchedule } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; import ComposeFormButton from './compose-form-button'; @@ -19,7 +17,7 @@ interface IScheduleButton { const ScheduleButton: React.FC = ({ composeId, disabled }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); @@ -27,11 +25,9 @@ const ScheduleButton: React.FC = ({ composeId, disabled }) => { const unavailable = !!compose.editedId; const handleClick = () => { - if (active) { - dispatch(removeSchedule(composeId)); - } else { - dispatch(addSchedule(composeId)); - } + updateCompose(composeId, (draft) => { + draft.scheduledAt = active ? null : new Date(Date.now() + 10 * 60 * 1000); + }); }; if (unavailable) { diff --git a/packages/pl-fe/src/features/compose/components/schedule-form.tsx b/packages/pl-fe/src/features/compose/components/schedule-form.tsx index d42f32b38..ade3e1bf7 100644 --- a/packages/pl-fe/src/features/compose/components/schedule-form.tsx +++ b/packages/pl-fe/src/features/compose/components/schedule-form.tsx @@ -2,13 +2,11 @@ import clsx from 'clsx'; import React, { Suspense, useCallback } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { setSchedule, removeSchedule } from '@/actions/compose'; import IconButton from '@/components/ui/icon-button'; import Input from '@/components/ui/input'; import { DatePicker } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useFeatures } from '@/hooks/use-features'; +import { useCompose, useComposeActions } from '@/stores/compose'; const isCurrentOrFutureDate = (date: Date) => date && new Date().setHours(0, 0, 0, 0) <= new Date(date).setHours(0, 0, 0, 0); @@ -29,7 +27,7 @@ interface IScheduleForm { } const ScheduleForm: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const intl = useIntl(); const features = useFeatures(); @@ -37,12 +35,15 @@ const ScheduleForm: React.FC = ({ composeId }) => { const active = !!scheduledAt; const onSchedule = (date: Date | null) => { - if (date === null) dispatch(removeSchedule(composeId)); - else dispatch(setSchedule(composeId, date)); + updateCompose(composeId, (draft) => { + draft.scheduledAt = date; + }); }; const handleRemove = (e: React.MouseEvent) => { - dispatch(removeSchedule(composeId)); + updateCompose(composeId, (draft) => { + draft.scheduledAt = null; + }); e.preventDefault(); }; diff --git a/packages/pl-fe/src/features/compose/components/sensitive-media-button.tsx b/packages/pl-fe/src/features/compose/components/sensitive-media-button.tsx index d34d79094..e68158188 100644 --- a/packages/pl-fe/src/features/compose/components/sensitive-media-button.tsx +++ b/packages/pl-fe/src/features/compose/components/sensitive-media-button.tsx @@ -1,9 +1,7 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { changeComposeSpoilerness } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; import ComposeFormButton from './compose-form-button'; @@ -21,11 +19,14 @@ interface ISensitiveMediaButton { const SensitiveMediaButton: React.FC = ({ composeId }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const active = useCompose(composeId).sensitive; - const onClick = () => dispatch(changeComposeSpoilerness(composeId)); + const onClick = () => + updateCompose(composeId, (draft) => { + draft.sensitive = !draft.sensitive; + }); return ( { +interface ISpoilerInput { composeId: string extends 'default' ? never : string; + theme?: InputThemes; } /** Text input for content warning in composer. */ -const SpoilerInput: React.FC = ({ - composeId, - onSuggestionsFetchRequested, - onSuggestionsClearRequested, - onSuggestionSelected, - theme, -}) => { +const SpoilerInput: React.FC = ({ composeId, theme }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); - const { language, modifiedLanguage, spoilerText, spoilerTextMap, suggestions } = - useCompose(composeId); + const { selectComposeSuggestion, updateCompose } = useComposeActions(); + const { language, modifiedLanguage, spoilerText, spoilerTextMap } = useCompose(composeId); + + const [token, setToken] = useState(''); + const suggestions = useComposeSuggestions(token); const handleChangeSpoilerText: React.ChangeEventHandler = (e) => { - dispatch(changeComposeSpoilerText(composeId, e.target.value)); + const text = e.target.value; + updateCompose(composeId, (draft) => { + if (!draft.modifiedLanguage || draft.modifiedLanguage === draft.language) { + draft.spoilerText = text; + } else { + draft.spoilerTextMap[draft.modifiedLanguage] = text; + } + }); + }; + + const onSuggestionsFetchRequested = (token: string) => setToken(token); + const onSuggestionsClearRequested = () => setToken(''); + const onSuggestionSelected = ( + tokenStart: number, + token: string | null, + value: AutoSuggestion, + ) => { + if (token && typeof token === 'string') { + selectComposeSuggestion(composeId, tokenStart, token, value, ['spoiler_text']); + } }; const value = diff --git a/packages/pl-fe/src/features/compose/components/upload-form.tsx b/packages/pl-fe/src/features/compose/components/upload-form.tsx index 7a46fe5df..c4608237e 100644 --- a/packages/pl-fe/src/features/compose/components/upload-form.tsx +++ b/packages/pl-fe/src/features/compose/components/upload-form.tsx @@ -1,10 +1,8 @@ import clsx from 'clsx'; import React, { useCallback, useRef } from 'react'; -import { changeMediaOrder } from '@/actions/compose'; import HStack from '@/components/ui/hstack'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useComposeActions } from '@/stores/compose'; import Upload from './upload'; import UploadProgress from './upload-progress'; @@ -15,7 +13,7 @@ interface IUploadForm { } const UploadForm: React.FC = ({ composeId, onSubmit }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const { isUploading, mediaAttachments } = useCompose(composeId); @@ -39,7 +37,12 @@ const UploadForm: React.FC = ({ composeId, onSubmit }) => { ); const handleDragEnd = useCallback(() => { - dispatch(changeMediaOrder(composeId, dragItem.current!, dragOverItem.current!)); + updateCompose(composeId, (draft) => { + const indexA = draft.mediaAttachments.findIndex((x) => x.id === dragItem.current!); + const indexB = draft.mediaAttachments.findIndex((x) => x.id === dragOverItem.current!); + const item = draft.mediaAttachments.splice(indexA, 1)[0]; + draft.mediaAttachments.splice(indexB, 0, item); + }); dragItem.current = null; dragOverItem.current = null; }, [dragItem, dragOverItem]); diff --git a/packages/pl-fe/src/features/compose/components/upload.tsx b/packages/pl-fe/src/features/compose/components/upload.tsx index f868bb1de..810390140 100644 --- a/packages/pl-fe/src/features/compose/components/upload.tsx +++ b/packages/pl-fe/src/features/compose/components/upload.tsx @@ -1,10 +1,8 @@ import React, { useCallback } from 'react'; -import { undoUploadCompose, changeUploadCompose } from '@/actions/compose'; import Upload from '@/components/upload'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useInstance } from '@/hooks/use-instance'; +import { useChangeUploadCompose, useCompose, useComposeActions } from '@/stores/compose'; interface IUploadCompose { id: string; @@ -23,7 +21,8 @@ const UploadCompose: React.FC = ({ onDragEnter, onDragEnd, }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); + const changeUploadCompose = useChangeUploadCompose(composeId); const { pleroma: { metadata: { description_limit: descriptionLimit }, @@ -33,18 +32,20 @@ const UploadCompose: React.FC = ({ const media = useCompose(composeId).mediaAttachments.find((item) => item.id === id)!; const handleDescriptionChange = (description: string, position?: [number, number]) => { - return dispatch( - changeUploadCompose(composeId, media.id, { - description, - focus: position - ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` - : undefined, - }), - ); + return changeUploadCompose(media.id, { + description, + focus: position + ? `${((position[0] - 0.5) * 2).toFixed(2)},${((position[1] - 0.5) * -2).toFixed(2)}` + : undefined, + }); }; const handleDelete = () => { - dispatch(undoUploadCompose(composeId, media.id)); + updateCompose(composeId, (draft) => { + const prevSize = draft.mediaAttachments.length; + draft.mediaAttachments = draft.mediaAttachments.filter((item) => item.id !== media.id); + if (prevSize === 1) draft.sensitive = false; + }); }; const handleDragStart = useCallback(() => { diff --git a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx index cd638bf32..5e451cb64 100644 --- a/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/preview-compose-container.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { cancelPreviewCompose } from '@/actions/compose'; import EventPreview from '@/components/event-preview'; import OutlineBox from '@/components/outline-box'; import QuotedStatusIndicator from '@/components/quoted-status-indicator'; @@ -15,9 +14,8 @@ import IconButton from '@/components/ui/icon-button'; import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import AccountContainer from '@/containers/account-container'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useCompose, useComposeActions } from '@/stores/compose'; import type { Status } from '@/normalizers/status'; @@ -34,14 +32,16 @@ interface IQuotedStatusContainer { /** Previewed status shown in post composer. */ const PreviewComposeContainer: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const intl = useIntl(); const { data: ownAccount } = useOwnAccount(); const previewedStatus = useCompose(composeId).preview as unknown as Status; const handleClose = () => { - dispatch(cancelPreviewCompose(composeId)); + updateCompose(composeId, (draft) => { + draft.preview = null; + }); }; const status = useMemo( diff --git a/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx b/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx index 9887194e8..9e44e958b 100644 --- a/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/quoted-status-container.tsx @@ -1,10 +1,9 @@ import React, { useCallback } from 'react'; -import { cancelQuoteCompose } from '@/actions/compose'; import QuotedStatus from '@/components/quoted-status'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { makeGetStatus } from '@/selectors'; +import { useCompose, useComposeActions } from '@/stores/compose'; interface IQuotedStatusContainer { composeId: string; @@ -12,15 +11,17 @@ interface IQuotedStatusContainer { /** QuotedStatus shown in post composer. */ const QuotedStatusContainer: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const getStatus = useCallback(makeGetStatus(), []); + const { quoteId } = useCompose(composeId); - const status = useAppSelector((state) => - getStatus(state, { id: state.compose[composeId]?.quoteId! }), - ); + const status = useAppSelector((state) => getStatus(state, { id: quoteId! })); const onCancel = () => { - dispatch(cancelQuoteCompose(composeId)); + updateCompose(composeId, (draft) => { + if (draft.quoteId) draft.dismissedQuotes.push(draft.quoteId); + draft.quoteId = null; + }); }; if (!status) { diff --git a/packages/pl-fe/src/features/compose/containers/reply-indicator-container.tsx b/packages/pl-fe/src/features/compose/containers/reply-indicator-container.tsx index 48adced17..9bfad5d34 100644 --- a/packages/pl-fe/src/features/compose/containers/reply-indicator-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/reply-indicator-container.tsx @@ -1,10 +1,8 @@ import React, { useCallback } from 'react'; -import { cancelReplyCompose } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; -import { useCompose } from '@/hooks/use-compose'; import { makeGetStatus } from '@/selectors'; +import { useCompose, useComposeActions } from '@/stores/compose'; import ReplyIndicator from '../components/reply-indicator'; @@ -17,10 +15,10 @@ const ReplyIndicatorContainer: React.FC = ({ composeId const { inReplyToId, editedId } = useCompose(composeId); const status = useAppSelector((state) => getStatus(state, { id: inReplyToId! })); - const dispatch = useAppDispatch(); + const { resetCompose } = useComposeActions(); const onCancel = () => { - dispatch(cancelReplyCompose()); + resetCompose('compose-modal'); }; if (!status) return null; diff --git a/packages/pl-fe/src/features/compose/containers/upload-button-container.tsx b/packages/pl-fe/src/features/compose/containers/upload-button-container.tsx index 5f7a57e7e..8b6c445c5 100644 --- a/packages/pl-fe/src/features/compose/containers/upload-button-container.tsx +++ b/packages/pl-fe/src/features/compose/containers/upload-button-container.tsx @@ -1,8 +1,6 @@ import React from 'react'; -import { uploadCompose } from '@/actions/compose'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useCompose, useUploadCompose } from '@/stores/compose'; import UploadButton from '../components/upload-button'; @@ -13,11 +11,11 @@ interface IUploadButtonContainer { } const UploadButtonContainer: React.FC = ({ composeId }) => { - const dispatch = useAppDispatch(); const { isUploading, resetFileKey } = useCompose(composeId); + const uploadCompose = useUploadCompose(composeId); const onSelectFile = (files: FileList, intl: IntlShape) => { - dispatch(uploadCompose(composeId, files, intl)); + uploadCompose(files); }; return ( diff --git a/packages/pl-fe/src/features/compose/editor/index.tsx b/packages/pl-fe/src/features/compose/editor/index.tsx index f76ab8976..f9b704cda 100644 --- a/packages/pl-fe/src/features/compose/editor/index.tsx +++ b/packages/pl-fe/src/features/compose/editor/index.tsx @@ -27,9 +27,9 @@ import { import React, { useMemo, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCompose } from '@/hooks/use-compose'; import { usePrevious } from '@/hooks/use-previous'; +import { useComposeStore } from '@/stores/compose'; import { useNodes } from './nodes'; import AutosuggestPlugin from './plugins/autosuggest-plugin'; @@ -110,7 +110,6 @@ const ComposeEditor = React.forwardRef( }, ref, ) => { - const dispatch = useAppDispatch(); const { contentType, modifiedLanguage: language } = useCompose(composeId); const isWysiwyg = contentType === 'wysiwyg'; const previouslyWasWysiwyg = usePrevious(isWysiwyg); @@ -125,9 +124,8 @@ const ComposeEditor = React.forwardRef( onError: console.error, nodes, theme, - editorState: dispatch((_, getState) => { - const state = getState(); - const compose = state.compose[composeId]; + editorState: (() => { + const compose = useComposeStore.getState().actions.getCompose(composeId); if (!compose) return; @@ -157,7 +155,7 @@ const ComposeEditor = React.forwardRef( $getRoot().clear().append(paragraph); } }; - }), + })(), }), [composeId, isWysiwyg], ); @@ -228,7 +226,6 @@ const ComposeEditor = React.forwardRef( diff --git a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx index 62a795e63..f5b9f2e13 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -32,11 +32,10 @@ import React, { } from 'react'; import ReactDOM from 'react-dom'; -import { clearComposeSuggestions, fetchComposeSuggestions } from '@/actions/compose'; import { saveSettings } from '@/actions/settings'; import AutosuggestEmoji from '@/components/autosuggest-emoji'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; +import { useComposeSuggestions } from '@/hooks/use-compose-suggestions'; import { queryClient } from '@/queries/client'; import { useSettingsStoreActions } from '@/stores/settings'; import { textAtCursorMatchesToken } from '@/utils/suggestions'; @@ -45,11 +44,10 @@ import AutosuggestAccount from '../../components/autosuggest-account'; import { $createEmojiNode } from '../nodes/emoji-node'; import { $createMentionNode } from '../nodes/mention-node'; +import type { AutoSuggestion } from '@/components/autosuggest-input'; import type { Emoji } from '@/features/emoji'; import type { Account } from 'pl-api'; -type AutoSuggestion = string | Emoji; - type QueryMatch = { leadOffset: number; matchingString: string; @@ -264,23 +262,22 @@ const useMenuAnchorRef = ( }; type AutosuggestPluginProps = { - composeId: string; suggestionsHidden: boolean; setSuggestionsHidden: (value: boolean) => void; }; const AutosuggestPlugin = ({ - composeId, suggestionsHidden, setSuggestionsHidden, }: AutosuggestPluginProps): React.JSX.Element | null => { const { rememberEmojiUse } = useSettingsStoreActions(); - const { suggestions } = useCompose(composeId); const dispatch = useAppDispatch(); const [editor] = useLexicalComposerContext(); const [resolution, setResolution] = useState(null); const [selectedSuggestion, setSelectedSuggestion] = useState(0); + const [token, setToken] = useState(''); + const suggestions = useComposeSuggestions(token); const anchorElementRef = useMenuAnchorRef(resolution, setResolution); const handleSelectSuggestion: React.MouseEventHandler = (e) => { @@ -293,39 +290,39 @@ const AutosuggestPlugin = ({ const suggestion = suggestions[index]; editor.update(() => { - dispatch((dispatch, getState) => { - const state = editor.getEditorState(); - const node = (state._selection as RangeSelection)?.anchor?.getNode(); - const { leadOffset, matchingString } = resolution!.match; - /** Offset for the beginning of the matched text, including the token. */ - const offset = leadOffset - 1; + const state = editor.getEditorState(); + const node = (state._selection as RangeSelection)?.anchor?.getNode(); + const { leadOffset, matchingString } = resolution!.match; + /** Offset for the beginning of the matched text, including the token. */ + const offset = leadOffset - 1; - /** Replace the matched text with the given node. */ - const replaceMatch = (replaceWith: LexicalNode) => { - const result = (node as TextNode).splitText(offset, offset + matchingString.length); - const textNode = result[1] ?? result[0]; - const replacedNode = textNode.replace(replaceWith); - replacedNode.insertAfter(new TextNode(' ')); - replacedNode.selectNext(); - }; + /** Replace the matched text with the given node. */ + const replaceMatch = (replaceWith: LexicalNode) => { + const result = (node as TextNode).splitText(offset, offset + matchingString.length); + const textNode = result[1] ?? result[0]; + const replacedNode = textNode.replace(replaceWith); + replacedNode.insertAfter(new TextNode(' ')); + replacedNode.selectNext(); + }; - if (typeof suggestion === 'object') { - if (!suggestion.id) return; + if (typeof suggestion === 'object' && 'id' in suggestion) { + if (!suggestion.id) return; - rememberEmojiUse(suggestion); - dispatch(saveSettings()); + rememberEmojiUse(suggestion as Emoji); + dispatch(saveSettings()); - replaceMatch($createEmojiNode(suggestion)); - } else if (suggestion[0] === '#') { + replaceMatch($createEmojiNode(suggestion as Emoji)); + } else if (typeof suggestion === 'string') { + if (suggestion[0] === '#') { (node as TextNode).setTextContent(`${suggestion} `); node.select(); } else { const account = queryClient.getQueryData(['accounts', suggestion]); if (account) replaceMatch($createMentionNode(account)); } + } - dispatch(clearComposeSuggestions(composeId)); - }); + setToken(''); }); }; @@ -353,15 +350,19 @@ const AutosuggestPlugin = ({ let inner: string | React.JSX.Element; let key: React.Key; - if (typeof suggestion === 'object') { - inner = ; + if (typeof suggestion === 'object' && 'id' in suggestion) { + inner = ; key = suggestion.id; - } else if (suggestion[0] === '#') { - inner = suggestion; - key = suggestion; + } else if (typeof suggestion === 'string') { + if (suggestion[0] === '#') { + inner = suggestion; + key = suggestion; + } else { + inner = ; + key = suggestion; + } } else { - inner = ; - key = suggestion; + return null; } return ( @@ -406,7 +407,7 @@ const AutosuggestPlugin = ({ return; } - dispatch(fetchComposeSuggestions(composeId, match.matchingString.trim())); + setToken(match.matchingString.trim()); if (!isSelectionOnEntityBoundary(editor, match.leadOffset)) { const isRangePositioned = tryToPositionRange(match.leadOffset, range); diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx index 6676f4208..99c29ce76 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-block-type-toolbar-plugin.tsx @@ -22,7 +22,7 @@ import * as React from 'react'; import { createPortal } from 'react-dom'; import { defineMessages, useIntl } from 'react-intl'; -import { uploadFile } from '@/actions/compose'; +import { uploadFile } from '@/actions/media'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; import { useInstance } from '@/hooks/use-instance'; diff --git a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx index 4b092e034..485eba2d4 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/state-plugin.tsx @@ -7,20 +7,19 @@ import debounce from 'lodash/debounce'; import { useCallback, useEffect } from 'react'; import { useIntl } from 'react-intl'; -import { - addSuggestedLanguage, - addSuggestedQuote, - setEditorState, - suggestClearLink, - suggestHashtagCasing, -} from '@/actions/compose'; import { fetchStatus } from '@/actions/statuses'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFeatures } from '@/hooks/use-features'; +import { useComposeStore } from '@/stores/compose'; import { useSettings } from '@/stores/settings'; import { getStatusIdsFromLinksInContent } from '@/utils/status'; import Purify from '@/utils/url-purify'; +import type { store } from '@/store'; + +let lazyStore: typeof store; +import('@/store').then(({ store }) => (lazyStore = store)).catch(() => {}); + import { TRANSFORMERS } from '../transformers'; import type { LanguageIdentificationModel } from 'fasttext.wasm.js/dist/models/language-identification/common.js'; @@ -38,54 +37,55 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const [editor] = useLexicalComposerContext(); const features = useFeatures(); const { urlPrivacy, ignoreHashtagCasingSuggestions } = useSettings(); + const { actions } = useComposeStore.getState(); const checkUrls = useCallback( debounce((editorState: EditorState) => { - dispatch((_, getState) => { - if (!urlPrivacy.clearLinksInCompose) return; + if (!urlPrivacy.clearLinksInCompose) return; - const state = getState(); - const compose = state.compose[composeId]; + const compose = actions.getCompose(composeId); - editorState.read(() => { - const compareUrl = (url: string) => { - const cleanUrl = Purify.clearUrl(url, true, false); - return { - originalUrl: url, - cleanUrl, - isDirty: cleanUrl !== url, - }; + editorState.read(() => { + const compareUrl = (url: string) => { + const cleanUrl = Purify.clearUrl(url, true, false); + return { + originalUrl: url, + cleanUrl, + isDirty: cleanUrl !== url, }; + }; - if (compose.clearLinkSuggestion?.key) { - const node = $getNodeByKey(compose.clearLinkSuggestion.key); - const url = (node as LinkNode | null)?.getURL?.(); - if (!url || node === null || !compareUrl(url).isDirty) { - dispatch(suggestClearLink(composeId, null)); - } else { - return; - } + if (compose.clearLinkSuggestion?.key) { + const node = $getNodeByKey(compose.clearLinkSuggestion.key); + const url = (node as LinkNode | null)?.getURL?.(); + if (!url || node === null || !compareUrl(url).isDirty) { + actions.updateCompose(composeId, (draft) => { + draft.clearLinkSuggestion = null; + }); + } else { + return; + } + } + + const links = [...$nodesOfType(AutoLinkNode), ...$nodesOfType(LinkNode)]; + + for (const link of links) { + if (compose.dismissedClearLinksSuggestions.includes(link.getKey())) { + continue; } - const links = [...$nodesOfType(AutoLinkNode), ...$nodesOfType(LinkNode)]; - - for (const link of links) { - if (compose.dismissedClearLinksSuggestions.includes(link.getKey())) { - continue; - } - - const { originalUrl, cleanUrl, isDirty } = compareUrl(link.getURL()); - if (!isDirty) { - continue; - } - - if (isDirty) { - return dispatch( - suggestClearLink(composeId, { key: link.getKey(), originalUrl, cleanUrl }), - ); - } + const { originalUrl, cleanUrl, isDirty } = compareUrl(link.getURL()); + if (!isDirty) { + continue; } - }); + + if (isDirty) { + actions.updateCompose(composeId, (draft) => { + draft.clearLinkSuggestion = { key: link.getKey(), originalUrl, cleanUrl }; + }); + return; + } + } }); }, 2000), [urlPrivacy.clearLinksInCompose], @@ -93,27 +93,28 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const checkHashtagCasingSuggestions = useCallback( debounce((editorState: EditorState) => { - dispatch((_, getState) => { - if (ignoreHashtagCasingSuggestions) return; + if (ignoreHashtagCasingSuggestions) return; - const state = getState(); - const compose = state.compose[composeId]; + const compose = actions.getCompose(composeId); - if (compose.hashtagCasingSuggestionIgnored) return; + if (compose.hashtagCasingSuggestionIgnored) return; - editorState.read(() => { - const hashtagNodes = $nodesOfType(HashtagNode); + editorState.read(() => { + const hashtagNodes = $nodesOfType(HashtagNode); - for (const tag of hashtagNodes) { - const text = tag.getTextContent(); + for (const tag of hashtagNodes) { + const text = tag.getTextContent(); - if (text.length > 10 && text.toLowerCase() === text && !text.match(/[0-9]/)) { - dispatch(suggestHashtagCasing(composeId, text)); - return; - } + if (text.length > 10 && text.toLowerCase() === text && !text.match(/[0-9]/)) { + actions.updateCompose(composeId, (draft) => { + draft.hashtagCasingSuggestion = text; + }); + return; } + } - dispatch(suggestHashtagCasing(composeId, null)); + actions.updateCompose(composeId, (draft) => { + draft.hashtagCasingSuggestion = null; }); }); }, 1000), @@ -122,19 +123,19 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const getQuoteSuggestions = useCallback( debounce((text: string) => { - dispatch(async (_, getState) => { - const state = getState(); - const compose = state.compose[composeId]; + const compose = actions.getCompose(composeId); - if (!features.quotePosts || compose?.quoteId) return; + if (!features.quotePosts || compose?.quoteId) return; - const ids = getStatusIdsFromLinksInContent(text); + const ids = getStatusIdsFromLinksInContent(text); + (async () => { let quoteId: string | undefined; for (const id of ids) { if (compose?.dismissedQuotes.includes(id)) continue; + const state = lazyStore.getState(); if (state.statuses[id]) { quoteId = id; break; @@ -148,24 +149,26 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { } } - if (quoteId) dispatch(addSuggestedQuote(composeId, quoteId)); - }); + if (quoteId) + actions.updateCompose(composeId, (draft) => { + draft.quoteId = quoteId!; + }); + })(); }, 2000), [], ); const detectLanguage = useCallback( debounce((text: string) => { - dispatch(async (dispatch, getState) => { - const state = getState(); - const compose = state.compose[composeId]; + const compose = actions.getCompose(composeId); - if (!features.postLanguages || features.languageDetection || compose?.language) return; + if (!features.postLanguages || features.languageDetection || compose?.language) return; - const wordsLength = text.split(/\s+/).length; + const wordsLength = text.split(/\s+/).length; - if (wordsLength < 4) return; + if (wordsLength < 4) return; + (async () => { if (!lidModel) { // eslint-disable-next-line import/extensions const { getLIDModel } = await import('fasttext.wasm.js/common'); @@ -175,9 +178,11 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { const { alpha2, possibility } = await lidModel.identify(text.replace(/\s+/i, ' ')); if (alpha2 && possibility > 0.5) { - dispatch(addSuggestedLanguage(composeId, alpha2)); + actions.updateCompose(composeId, (draft) => { + draft.suggestedLanguage = alpha2; + }); } - }); + })(); }, 750), [], ); @@ -192,7 +197,15 @@ const StatePlugin: React.FC = ({ composeId, isWysiwyg }) => { } const isEmpty = text === ''; const data = isEmpty ? null : JSON.stringify(editorState.toJSON()); - dispatch(setEditorState(composeId, data, text)); + actions.updateCompose(composeId, (draft) => { + if (!draft.modifiedLanguage || draft.modifiedLanguage === draft.language) { + draft.editorState = data as string; + draft.text = text; + } else if (draft.modifiedLanguage) { + draft.editorStateMap[draft.modifiedLanguage] = data as string; + draft.textMap[draft.modifiedLanguage] = text; + } + }); checkUrls(editorState); checkHashtagCasingSuggestions(editorState); getQuoteSuggestions(plainText); diff --git a/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx b/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx index 558d77d00..515d203e0 100644 --- a/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx +++ b/packages/pl-fe/src/features/draft-statuses/components/draft-status-action-bar.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; -import { setComposeToStatus } from '@/actions/compose'; import { fetchStatus } from '@/actions/statuses'; import Button from '@/components/ui/button'; import HStack from '@/components/ui/hstack'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useCancelDraftStatus } from '@/queries/statuses/use-draft-statuses'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; @@ -34,6 +34,7 @@ const DraftStatusActionBar: React.FC = ({ source, status const intl = useIntl(); const { openModal } = useModalsActions(); + const { setComposeToStatus } = useComposeActions(); const settings = useSettings(); const dispatch = useAppDispatch(); const cancelDraftStatus = useCancelDraftStatus(); @@ -54,18 +55,7 @@ const DraftStatusActionBar: React.FC = ({ source, status const handleEditClick = () => { if (status.in_reply_to_id) dispatch(fetchStatus(status.in_reply_to_id)); - dispatch( - setComposeToStatus( - status, - status.poll, - source.text, - source.spoiler_text, - source.content_type, - false, - source.draft_id, - source.editorState, - ), - ); + setComposeToStatus(status, status.poll, source, false, source.draft_id, source.editorState); openModal('COMPOSE'); }; diff --git a/packages/pl-fe/src/features/event/components/event-header.tsx b/packages/pl-fe/src/features/event/components/event-header.tsx index 0179dca24..041342773 100644 --- a/packages/pl-fe/src/features/event/components/event-header.tsx +++ b/packages/pl-fe/src/features/event/components/event-header.tsx @@ -2,7 +2,6 @@ import { Link, useNavigate } from '@tanstack/react-router'; import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { directCompose, mentionCompose, quoteCompose } from '@/actions/compose'; import { fetchEventIcs } from '@/actions/events'; import { deleteStatusModal, toggleStatusSensitivityModal } from '@/actions/moderation'; import { initReport, ReportableEntities } from '@/actions/reports'; @@ -29,6 +28,7 @@ import { useUnpinStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import copy from '@/utils/copy'; @@ -113,6 +113,7 @@ const EventHeader: React.FC = ({ status }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const navigate = useNavigate(); + const { quoteCompose, mentionCompose, directCompose } = useComposeActions(); const { openModal } = useModalsActions(); const { getOrCreateChatByAccountId } = useChats(); @@ -187,7 +188,7 @@ const EventHeader: React.FC = ({ status }) => { }; const handleQuoteClick = () => { - dispatch(quoteCompose(status)); + quoteCompose(status); }; const handlePinClick = () => { @@ -212,7 +213,7 @@ const EventHeader: React.FC = ({ status }) => { }; const handleMentionClick = () => { - dispatch(mentionCompose(account)); + mentionCompose(account); }; const handleChatClick = () => { @@ -222,7 +223,7 @@ const EventHeader: React.FC = ({ status }) => { }; const handleDirectClick = () => { - dispatch(directCompose(account)); + directCompose(account); }; const handleMuteClick = () => { diff --git a/packages/pl-fe/src/features/notifications/components/notification.tsx b/packages/pl-fe/src/features/notifications/components/notification.tsx index 3607ffd71..db49b3f32 100644 --- a/packages/pl-fe/src/features/notifications/components/notification.tsx +++ b/packages/pl-fe/src/features/notifications/components/notification.tsx @@ -9,7 +9,6 @@ import { MessageDescriptor, } from 'react-intl'; -import { mentionCompose, replyCompose } from '@/actions/compose'; import AttachmentThumbs from '@/components/attachment-thumbs'; import HoverAccountWrapper from '@/components/hover-account-wrapper'; import Icon from '@/components/icon'; @@ -22,7 +21,6 @@ import AccountContainer from '@/containers/account-container'; import StatusContainer from '@/containers/status-container'; import Emojify from '@/features/emoji/emojify'; import { Hotkeys } from '@/features/ui/components/hotkeys'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useInstance } from '@/hooks/use-instance'; import { useLoggedIn } from '@/hooks/use-logged-in'; @@ -33,6 +31,7 @@ import { useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; import { makeGetNotification } from '@/selectors'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; import { useStatusMetaActions } from '@/stores/status-meta'; @@ -288,7 +287,7 @@ const getNotificationStatus = ( const Notification: React.FC = (props) => { const { onMoveUp, onMoveDown, compact } = props; - const dispatch = useAppDispatch(); + const { mentionCompose, replyCompose } = useComposeActions(); const getNotification = useCallback(makeGetNotification(), []); @@ -336,7 +335,7 @@ const Notification: React.FC = (props) => { (e?: KeyboardEvent) => { e?.preventDefault(); - dispatch(mentionCompose(account)); + mentionCompose(account); }, [account], ); @@ -346,9 +345,9 @@ const Notification: React.FC = (props) => { e?.preventDefault(); if (status) { - dispatch(replyCompose(status, account)); + replyCompose(status, account); } else { - dispatch(mentionCompose(account)); + mentionCompose(account); } }, [account], diff --git a/packages/pl-fe/src/features/reply-mentions/account.tsx b/packages/pl-fe/src/features/reply-mentions/account.tsx index c5436ddf3..cdecc9587 100644 --- a/packages/pl-fe/src/features/reply-mentions/account.tsx +++ b/packages/pl-fe/src/features/reply-mentions/account.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { addToMentions, removeFromMentions } from '@/actions/compose'; import AccountComponent from '@/components/account'; import IconButton from '@/components/ui/icon-button'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useAccount } from '@/queries/accounts/use-account'; +import { useCompose, useComposeActions } from '@/stores/compose'; const messages = defineMessages({ remove: { id: 'reply_mentions.account.remove', defaultMessage: 'Remove from mentions' }, @@ -21,14 +19,25 @@ interface IAccount { const Account: React.FC = ({ composeId, accountId, author }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const compose = useCompose(composeId); const { data: account } = useAccount(accountId); const added = !!account && compose.to?.includes(account.acct); - const onRemove = () => dispatch(removeFromMentions(composeId, accountId)); - const onAdd = () => dispatch(addToMentions(composeId, accountId)); + const onRemove = () => + updateCompose(composeId, (draft) => { + if (account) { + draft.to = draft.to?.filter((acct) => acct !== account.acct) || []; + } + }); + const onAdd = () => + updateCompose(composeId, (draft) => { + if (account) { + if (draft.to?.includes(account.acct)) return; + draft.to = [...(draft.to || []), account.acct]; + } + }); if (!account) return null; diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index 402eca2bc..c072a7603 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { Helmet } from 'react-helmet-async'; import { useIntl } from 'react-intl'; -import { type ComposeReplyAction, mentionCompose, replyCompose } from '@/actions/compose'; import ScrollableList from '@/components/scrollable-list'; import StatusActionBar from '@/components/status-action-bar'; import Tombstone from '@/components/tombstone'; @@ -12,13 +11,13 @@ import Stack from '@/components/ui/stack'; import PlaceholderStatus from '@/features/placeholder/components/placeholder-status'; import { Hotkeys } from '@/features/ui/components/hotkeys'; import PendingStatus from '@/features/ui/components/pending-status'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useFavouriteStatus, useReblogStatus, useUnfavouriteStatus, useUnreblogStatus, } from '@/queries/statuses/use-status-interactions'; +import { useComposeActions } from '@/stores/compose'; import { useThread } from '@/stores/contexts'; import { useModalsActions } from '@/stores/modals'; import { useSettings } from '@/stores/settings'; @@ -49,9 +48,9 @@ const Thread = ({ withMedia = true, setExpandAllStatuses, }: IThread) => { - const dispatch = useAppDispatch(); const navigate = useNavigate(); const intl = useIntl(); + const { replyCompose, mentionCompose } = useComposeActions(); const { expandStatuses, revealStatusesMedia, toggleStatusesMediaHidden } = useStatusMetaActions(); const { openModal } = useModalsActions(); @@ -80,8 +79,8 @@ const Thread = ({ else favouriteStatus(); }; - const handleReplyClick = (status: ComposeReplyAction['status']) => { - dispatch(replyCompose(status)); + const handleReplyClick = (status: Parameters[0]) => { + replyCompose(status); }; const handleReblogClick = (status: SelectedStatus, e?: React.MouseEvent) => { @@ -100,7 +99,7 @@ const Thread = ({ }; const handleMentionClick = (account: Pick) => { - dispatch(mentionCompose(account)); + mentionCompose(account); }; const handleHotkeyOpenMedia = (e?: KeyboardEvent) => { diff --git a/packages/pl-fe/src/features/ui/components/compose-button.tsx b/packages/pl-fe/src/features/ui/components/compose-button.tsx index 3748e352a..bb4f09d6d 100644 --- a/packages/pl-fe/src/features/ui/components/compose-button.tsx +++ b/packages/pl-fe/src/features/ui/components/compose-button.tsx @@ -2,12 +2,11 @@ import { useMatch } from '@tanstack/react-router'; import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { groupComposeModal } from '@/actions/compose'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; import Icon from '@/components/ui/icon'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useGroupQuery } from '@/queries/groups/use-group'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { layouts } from '../router'; @@ -47,14 +46,14 @@ const HomeComposeButton: React.FC = ({ shrink }) => { }; const GroupComposeButton: React.FC = ({ shrink }) => { - const dispatch = useAppDispatch(); + const { groupComposeModal } = useComposeActions(); const match = useMatch({ from: layouts.group.id, shouldThrow: false }); const { data: group } = useGroupQuery(match?.params.groupId); if (!group) return null; const onOpenCompose = () => { - dispatch(groupComposeModal(group)); + groupComposeModal(group); }; return ( diff --git a/packages/pl-fe/src/features/ui/components/modal-root.tsx b/packages/pl-fe/src/features/ui/components/modal-root.tsx index b1a3a3217..a6c3395d6 100644 --- a/packages/pl-fe/src/features/ui/components/modal-root.tsx +++ b/packages/pl-fe/src/features/ui/components/modal-root.tsx @@ -1,8 +1,7 @@ import React, { Suspense, lazy } from 'react'; -import { cancelReplyCompose } from '@/actions/compose'; import Base from '@/components/modal-root'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useComposeStore } from '@/stores/compose'; import { useModals, useModalsActions } from '@/stores/modals'; import ModalLoading from './modal-loading'; @@ -62,7 +61,6 @@ const ModalRoot: React.FC = () => { const renderLoading = (modalId: string) => !['MEDIA', 'BOOST', 'CONFIRM'].includes(modalId) ? : null; - const dispatch = useAppDispatch(); const modals = useModals(); const { closeModal } = useModalsActions(); const { modalType: type, modalProps: props } = modals.at(-1) ?? { @@ -74,7 +72,7 @@ const ModalRoot: React.FC = () => { const onClickClose = (type?: ModalType, all?: boolean) => { switch (type) { case 'COMPOSE': - dispatch(cancelReplyCompose()); + useComposeStore.getState().actions.resetCompose('compose-modal'); break; default: break; diff --git a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx index 4f01fa91a..c9c53b999 100644 --- a/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx +++ b/packages/pl-fe/src/features/ui/util/global-hotkeys.tsx @@ -1,10 +1,9 @@ import { useNavigate, useRouter } from '@tanstack/react-router'; import React, { useMemo } from 'react'; -import { resetCompose } from '@/actions/compose'; import { FOCUS_EDITOR_COMMAND } from '@/features/compose/editor/plugins/focus-plugin'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import { Hotkeys } from '../components/hotkeys'; @@ -45,9 +44,9 @@ interface IGlobalHotkeys { const GlobalHotkeys: React.FC = ({ children, node }) => { const navigate = useNavigate(); const { history } = useRouter(); - const dispatch = useAppDispatch(); const { data: account } = useOwnAccount(); const { openModal } = useModalsActions(); + const { resetCompose } = useComposeActions(); const handlers = useMemo(() => { const handleHotkeyNew = (e?: KeyboardEvent) => { @@ -84,7 +83,7 @@ const GlobalHotkeys: React.FC = ({ children, node }) => { const handleHotkeyForceNew = (e?: KeyboardEvent) => { const composeId = handleHotkeyNew(e); - dispatch(resetCompose(composeId ?? undefined)); + resetCompose(composeId ?? undefined); }; const handleHotkeyBack = () => { diff --git a/packages/pl-fe/src/hooks/use-compose-suggestions.ts b/packages/pl-fe/src/hooks/use-compose-suggestions.ts new file mode 100644 index 000000000..5b86ded29 --- /dev/null +++ b/packages/pl-fe/src/hooks/use-compose-suggestions.ts @@ -0,0 +1,54 @@ +import { useMemo } from 'react'; + +import { AutoSuggestion } from '@/components/autosuggest-input'; +import emojiSearch from '@/features/emoji/search'; +import { useDebounce } from '@/hooks/use-debounce'; +import { useCustomEmojis } from '@/queries/instance/use-custom-emojis'; +import { useSearchHashtags } from '@/queries/search/use-search'; +import { useAccountSearch } from '@/queries/search/use-search-accounts'; +import useTrends from '@/queries/trends'; + +const useComposeSuggestions = (token: string): Array => { + const debouncedToken = useDebounce(token, 300); + + const searchedType = token.startsWith('@') + ? 'accounts' + : token.startsWith('#') + ? 'hashtags' + : token.startsWith(':') + ? 'emojis' + : null; + + // TODO: fix default selectors across the code + const { data: customEmojis } = useCustomEmojis((emojis) => emojis); + const { data: accountIds } = useAccountSearch(searchedType === 'accounts' ? debouncedToken : '', { + resolve: false, + limit: 5, + }); + const { data: trendingTags } = useTrends(); + const { data: searchResult } = useSearchHashtags( + searchedType === 'hashtags' ? debouncedToken : '', + ); + + return useMemo((): Array => { + if (searchedType === 'emojis') { + return emojiSearch(token.replace(':', ''), { maxResults: 10 }, customEmojis); + } + + if (searchedType === 'accounts') { + return accountIds ?? []; + } + + if (searchedType === 'hashtags') { + if (token.length === 1) { + return (trendingTags ?? []).map(({ name }) => `#${name}`); + } + + return (searchResult ?? []).map(({ name }) => `#${name}`); + } + + return []; + }, [searchedType, token, customEmojis, accountIds, trendingTags, searchResult]); +}; + +export { useComposeSuggestions }; diff --git a/packages/pl-fe/src/hooks/use-compose.ts b/packages/pl-fe/src/hooks/use-compose.ts index d291fe05a..d74444e70 100644 --- a/packages/pl-fe/src/hooks/use-compose.ts +++ b/packages/pl-fe/src/hooks/use-compose.ts @@ -1,9 +1,2 @@ -import { useAppSelector } from './use-app-selector'; - -import type { Compose } from '@/reducers/compose'; - -/** Get compose for given key with fallback to 'default' */ -const useCompose = (composeId: ID extends 'default' ? never : ID): Compose => - useAppSelector((state) => state.compose[composeId] || state.compose.default); - -export { useCompose }; +// Re-export useCompose from the Zustand store +export { useCompose } from '@/stores/compose'; diff --git a/packages/pl-fe/src/layouts/home-layout.tsx b/packages/pl-fe/src/layouts/home-layout.tsx index 80d40fe59..cab57f6f1 100644 --- a/packages/pl-fe/src/layouts/home-layout.tsx +++ b/packages/pl-fe/src/layouts/home-layout.tsx @@ -1,9 +1,7 @@ import { Outlet, Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useRef } from 'react'; -import { useIntl } from 'react-intl'; -import { uploadCompose } from '@/actions/compose'; import { BANNER_HTML } from '@/build-config'; import Avatar from '@/components/ui/avatar'; import Layout from '@/components/ui/layout'; @@ -20,18 +18,15 @@ import { AnnouncementsPanel, ComposeForm, } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { useFeatures } from '@/hooks/use-features'; import { useFrontendConfig } from '@/hooks/use-frontend-config'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { useUploadCompose } from '@/stores/compose'; import { useSettings } from '@/stores/settings'; const HomeLayout = () => { - const intl = useIntl(); - const dispatch = useAppDispatch(); - const me = useAppSelector((state) => state.me); const { data: account } = useOwnAccount(); const features = useFeatures(); @@ -41,11 +36,13 @@ const HomeLayout = () => { const composeId = 'home'; const composeBlock = useRef(null); + const uploadCompose = useUploadCompose(composeId); + const hasCrypto = typeof frontendConfig.cryptoAddresses[0]?.ticker === 'string'; const cryptoLimit = frontendConfig.cryptoDonatePanel.limit; const { isDragging, isDraggedOver } = useDraggedFiles(composeBlock, (files) => { - dispatch(uploadCompose(composeId, files, intl)); + uploadCompose(files); }); const acct = account ? account.acct : ''; diff --git a/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx b/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx index 662fba57a..bf0802fd8 100644 --- a/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx +++ b/packages/pl-fe/src/modals/compose-interaction-policy-modal.tsx @@ -1,17 +1,12 @@ import { Link } from '@tanstack/react-router'; +import { create } from 'mutative'; import React, { useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { - changeComposeInteractionPolicyOption, - changeComposeQuotePolicyOption, -} from '@/actions/compose'; import Modal from '@/components/ui/modal'; import Stack from '@/components/ui/stack'; import Warning from '@/features/compose/components/warning'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useClient } from '@/hooks/use-client'; -import { useCompose } from '@/hooks/use-compose'; import { InteractionPolicyConfig, type Policy, @@ -19,9 +14,10 @@ import { type Scope, } from '@/pages/settings/interaction-policies'; import { useInteractionPolicies } from '@/queries/settings/use-interaction-policies'; +import { useCompose, useComposeActions } from '@/stores/compose'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; -import type { CreateStatusParams } from 'pl-api'; +import type { CreateStatusParams, InteractionPolicy } from 'pl-api'; const MANAGABLE_VISIBILITIES = ['public', 'unlisted', 'private']; @@ -33,7 +29,7 @@ const ComposeInteractionPolicyModal: React.FC< BaseModalProps & ComposeInteractionPolicyModalProps > = ({ composeId, onClose }) => { const client = useClient(); - const dispatch = useAppDispatch(); + const { updateCompose } = useComposeActions(); const [initialQuotePolicy, setInitialQuotePolicy] = useState(undefined); const { interactionPolicies: initial } = useInteractionPolicies(); @@ -65,13 +61,25 @@ const ComposeInteractionPolicyModal: React.FC< }; const onChange = (policy: Policy, rule: Rule, value: Scope[]) => { - dispatch( - changeComposeInteractionPolicyOption(composeId, policy, rule, value, interactionPolicy), - ); + updateCompose(composeId, (draft) => { + draft.interactionPolicy ??= JSON.parse(JSON.stringify(interactionPolicy))!; + + draft.interactionPolicy = create( + draft.interactionPolicy ?? interactionPolicy, + (draftPolicy: InteractionPolicy) => { + draftPolicy[policy][rule] = value; + draftPolicy[policy][rule === 'always' ? 'with_approval' : 'always'] = draftPolicy[policy][ + rule === 'always' ? 'with_approval' : 'always' + ].filter((r) => !value.includes(r as any)); + }, + ); + }); }; const onQuotePolicyChange = (value: CreateStatusParams['quote_approval_policy']) => { - dispatch(changeComposeQuotePolicyOption(composeId, value)); + updateCompose(composeId, (draft) => { + draft.quoteApprovalPolicy = value; + }); }; return ( diff --git a/packages/pl-fe/src/modals/compose-modal.tsx b/packages/pl-fe/src/modals/compose-modal.tsx index 71cae843a..8d4f2e224 100644 --- a/packages/pl-fe/src/modals/compose-modal.tsx +++ b/packages/pl-fe/src/modals/compose-modal.tsx @@ -2,14 +2,12 @@ import clsx from 'clsx'; import React, { useRef } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { cancelReplyCompose, uploadCompose } from '@/actions/compose'; import { checkComposeContent } from '@/components/modal-root'; import Modal from '@/components/ui/modal'; import { ComposeForm } from '@/features/ui/util/async-components'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; -import { useCompose } from '@/hooks/use-compose'; import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { usePersistDraftStatus } from '@/queries/statuses/use-draft-statuses'; +import { useCompose, useComposeActions, useUploadCompose } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; @@ -29,16 +27,17 @@ const ComposeModal: React.FC = ({ composeId = 'compose-modal', }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); const node = useRef(null); const compose = useCompose(composeId); + const uploadCompose = useUploadCompose(composeId); + const { resetCompose } = useComposeActions(); const { openModal } = useModalsActions(); const persistDraftStatus = usePersistDraftStatus(); const { editedId, visibility, inReplyToId, quoteId, groupId } = compose; const { isDragging, isDraggedOver } = useDraggedFiles(node, (files) => { - dispatch(uploadCompose(composeId, files, intl)); + uploadCompose(files); }); const onClickClose = () => { @@ -76,7 +75,7 @@ const ComposeModal: React.FC = ({ confirm: intl.formatMessage(editedId ? messages.cancelEditing : messages.confirm), onConfirm: () => { onClose('COMPOSE'); - dispatch(cancelReplyCompose()); + resetCompose('compose-modal'); }, secondary: intl.formatMessage(messages.saveDraft), onSecondary: editedId @@ -84,7 +83,7 @@ const ComposeModal: React.FC = ({ : () => { persistDraftStatus(composeId); onClose('COMPOSE'); - dispatch(cancelReplyCompose()); + resetCompose('compose-modal'); }, }); } else { diff --git a/packages/pl-fe/src/modals/reply-mentions-modal.tsx b/packages/pl-fe/src/modals/reply-mentions-modal.tsx index 29ec7294b..748df4487 100644 --- a/packages/pl-fe/src/modals/reply-mentions-modal.tsx +++ b/packages/pl-fe/src/modals/reply-mentions-modal.tsx @@ -6,8 +6,8 @@ import Account from '@/features/reply-mentions/account'; import { useAppSelector } from '@/hooks/use-app-selector'; import { useCompose } from '@/hooks/use-compose'; import { useOwnAccount } from '@/hooks/use-own-account'; -import { statusToMentionsAccountIdsArray } from '@/reducers/compose'; import { makeGetStatus } from '@/selectors'; +import { statusToMentionsAccountIdsArray } from '@/stores/compose'; import type { BaseModalProps } from '@/features/ui/components/modal-root'; diff --git a/packages/pl-fe/src/pages/fun/circle.tsx b/packages/pl-fe/src/pages/fun/circle.tsx index 25608cbb6..611ff3609 100644 --- a/packages/pl-fe/src/pages/fun/circle.tsx +++ b/packages/pl-fe/src/pages/fun/circle.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useRef, useState } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; import { processCircle } from '@/actions/circle'; -import { resetCompose, uploadComposeSuccess, uploadFile } from '@/actions/compose'; +import { uploadFile } from '@/actions/media'; import Account from '@/components/account'; import Accordion from '@/components/ui/accordion'; import Avatar from '@/components/ui/avatar'; @@ -17,6 +17,7 @@ import Stack from '@/components/ui/stack'; import Text from '@/components/ui/text'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useOwnAccount } from '@/hooks/use-own-account'; +import { appendMedia, useComposeActions } from '@/stores/compose'; import { useModalsActions } from '@/stores/modals'; import toast from '@/toast'; @@ -76,6 +77,7 @@ const CirclePage: React.FC = () => { const canvasRef = useRef(null); const { openModal } = useModalsActions(); + const { resetCompose, updateCompose } = useComposeActions(); const { data: account } = useOwnAccount(); useEffect(() => {}, []); @@ -92,14 +94,16 @@ const CirclePage: React.FC = () => { const onCompose: React.MouseEventHandler = (e) => { e.preventDefault(); - dispatch(resetCompose('compose-modal')); + resetCompose('compose-modal'); canvasRef.current!.toBlob((blob) => { const file = new File([blob!], 'interactions_circle.png', { type: 'image/png' }); dispatch( uploadFile(file, intl, (data) => { - dispatch(uploadComposeSuccess('compose-modal', data)); + updateCompose('compose-modal', (draft) => { + appendMedia(draft, data); + }); openModal('COMPOSE'); }), ); diff --git a/packages/pl-fe/src/pages/statuses/compose-event.tsx b/packages/pl-fe/src/pages/statuses/compose-event.tsx index bcbab2287..626705f15 100644 --- a/packages/pl-fe/src/pages/statuses/compose-event.tsx +++ b/packages/pl-fe/src/pages/statuses/compose-event.tsx @@ -8,7 +8,6 @@ import Tabs from '@/components/ui/tabs'; import { EditEvent } from '@/features/compose-event/tabs/edit-event'; import { ManagePendingParticipants } from '@/features/compose-event/tabs/manage-pending-participants'; import { eventEditRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; const messages = defineMessages({ manageEvent: { id: 'navigation_bar.manage_event', defaultMessage: 'Manage event' }, @@ -19,7 +18,6 @@ const messages = defineMessages({ const EditEventPage = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); const { statusId } = eventEditRoute.useParams(); @@ -27,7 +25,7 @@ const EditEventPage = () => { useEffect( () => () => { - dispatch(cancelEventCompose()); + cancelEventCompose(); }, [statusId], ); @@ -69,11 +67,10 @@ const EditEventPage = () => { const ComposeEventPage = () => { const intl = useIntl(); - const dispatch = useAppDispatch(); useEffect( () => () => { - dispatch(cancelEventCompose()); + cancelEventCompose(); }, [], ); diff --git a/packages/pl-fe/src/pages/statuses/event-discussion.tsx b/packages/pl-fe/src/pages/statuses/event-discussion.tsx index 729316972..531a8338d 100644 --- a/packages/pl-fe/src/pages/statuses/event-discussion.tsx +++ b/packages/pl-fe/src/pages/statuses/event-discussion.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; -import { eventDiscussionCompose } from '@/actions/compose'; import { fetchStatusWithContext } from '@/actions/statuses'; import MissingIndicator from '@/components/missing-indicator'; import ScrollableList from '@/components/scrollable-list'; @@ -15,6 +14,7 @@ import { ComposeForm } from '@/features/ui/util/async-components'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useAppSelector } from '@/hooks/use-app-selector'; import { makeGetStatus } from '@/selectors'; +import { useComposeActions } from '@/stores/compose'; import { useDescendantsIds } from '@/stores/contexts'; import { selectChild } from '@/utils/scroll-utils'; @@ -25,6 +25,7 @@ const EventDiscussionPage: React.FC = () => { const intl = useIntl(); const dispatch = useAppDispatch(); + const { eventDiscussionCompose } = useComposeActions(); const getStatus = useCallback(makeGetStatus(), []); const status = useAppSelector((state) => getStatus(state, { id: statusId })); @@ -51,7 +52,7 @@ const EventDiscussionPage: React.FC = () => { }, [statusId]); useEffect(() => { - if (isLoaded && me) dispatch(eventDiscussionCompose(`reply:${statusId}`, status!)); + if (isLoaded && me) eventDiscussionCompose(`reply:${statusId}`, status!); }, [isLoaded, me]); const handleMoveUp = (id: string) => { diff --git a/packages/pl-fe/src/pages/timelines/group-timeline.tsx b/packages/pl-fe/src/pages/timelines/group-timeline.tsx index 7cb151890..4d7fbe037 100644 --- a/packages/pl-fe/src/pages/timelines/group-timeline.tsx +++ b/packages/pl-fe/src/pages/timelines/group-timeline.tsx @@ -1,9 +1,8 @@ import { Link } from '@tanstack/react-router'; import clsx from 'clsx'; import React, { useEffect, useRef } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { FormattedMessage } from 'react-intl'; -import { groupCompose, uploadCompose } from '@/actions/compose'; import { fetchGroupTimeline } from '@/actions/timelines'; import { useGroupStream } from '@/api/hooks/streaming/use-group-stream'; import Avatar from '@/components/ui/avatar'; @@ -18,27 +17,30 @@ import { useDraggedFiles } from '@/hooks/use-dragged-files'; import { useOwnAccount } from '@/hooks/use-own-account'; import { useGroupQuery } from '@/queries/groups/use-group'; import { makeGetStatusIds } from '@/selectors'; +import { useComposeActions, useUploadCompose } from '@/stores/compose'; const getStatusIds = makeGetStatusIds(); const GroupTimelinePage: React.FC = () => { const { groupId } = groupTimelineRoute.useParams(); - const intl = useIntl(); + const composeId = `group:${groupId}`; + const { data: account } = useOwnAccount(); const dispatch = useAppDispatch(); + const uploadCompose = useUploadCompose(composeId); + const { updateCompose } = useComposeActions(); const composer = useRef(null); const { data: group } = useGroupQuery(groupId); - const composeId = `group:${groupId}`; const canComposeGroupStatus = !!account && group?.relationship?.member; const featuredStatusIds = useAppSelector((state) => getStatusIds(state, { type: `group:${group?.id}:pinned` }), ); const { isDragging, isDraggedOver } = useDraggedFiles(composer, (files) => { - dispatch(uploadCompose(composeId, files, intl)); + uploadCompose(files); }); const handleLoadMore = (maxId: string) => { @@ -50,7 +52,11 @@ const GroupTimelinePage: React.FC = () => { useEffect(() => { dispatch(fetchGroupTimeline(groupId, {})); // dispatch(fetchGroupTimeline(groupId, { pinned: true })); - dispatch(groupCompose(composeId, groupId)); + updateCompose(composeId, (draft) => { + draft.visibility = 'group'; + draft.groupId = groupId; + draft.caretPosition = null; + }); }, [groupId]); if (!group) { diff --git a/packages/pl-fe/src/pages/utils/share.tsx b/packages/pl-fe/src/pages/utils/share.tsx index ae2874d04..983ec5835 100644 --- a/packages/pl-fe/src/pages/utils/share.tsx +++ b/packages/pl-fe/src/pages/utils/share.tsx @@ -1,12 +1,11 @@ import { useNavigate } from '@tanstack/react-router'; import React, { useEffect } from 'react'; -import { openComposeWithText } from '@/actions/compose'; import { shareRoute } from '@/features/ui/router'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useComposeActions } from '@/stores/compose'; const SharePage: React.FC = () => { - const dispatch = useAppDispatch(); + const { openComposeWithText } = useComposeActions(); const navigate = useNavigate(); const params = shareRoute.useSearch(); @@ -17,7 +16,7 @@ const SharePage: React.FC = () => { navigate({ to: '/', replace: true }); if (text) { - dispatch(openComposeWithText('compose-modal', text)); + openComposeWithText('compose-modal', text); } }, []); diff --git a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts index ce8532daf..ea2a3750c 100644 --- a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts +++ b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts @@ -3,10 +3,10 @@ import { create } from 'mutative'; import { mediaAttachmentSchema } from 'pl-api'; import * as v from 'valibot'; -import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useOwnAccount } from '@/hooks/use-own-account'; import { filteredArray } from '@/schemas/utils'; import KVStore from '@/storage/kv-store'; +import { useComposeStore } from '@/stores/compose'; import { APIEntity } from '@/types/entities'; const draftStatusSchema = v.pipe( @@ -72,27 +72,24 @@ const useDraftStatusesCountQuery = () => const usePersistDraftStatus = () => { const { data: account } = useOwnAccount(); - const dispatch = useAppDispatch(); const queryClient = useQueryClient(); return (composeId: string) => { - dispatch((_, getState) => { - const compose = getState().compose[composeId]; + const compose = useComposeStore.getState().actions.getCompose(composeId); - const draft = { - ...compose, - draft_id: compose.draftId ?? crypto.randomUUID(), - }; + const draft = { + ...compose, + draft_id: compose.draftId ?? crypto.randomUUID(), + }; - const drafts = queryClient.getQueryData>(['draftStatuses']) ?? {}; + const drafts = queryClient.getQueryData>(['draftStatuses']) ?? {}; - const newDrafts: Record = create(drafts, (oldDrafts) => { - oldDrafts[draft.draft_id] = v.parse(draftStatusSchema, draft); - }); - return persistDrafts(account!.url, newDrafts).then(() => - queryClient.invalidateQueries({ queryKey: ['draftStatuses'] }), - ); + const newDrafts: Record = create(drafts, (oldDrafts) => { + oldDrafts[draft.draft_id] = v.parse(draftStatusSchema, draft); }); + return persistDrafts(account!.url, newDrafts).then(() => + queryClient.invalidateQueries({ queryKey: ['draftStatuses'] }), + ); }; }; diff --git a/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts b/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts index a3ca72f5a..cc48f0c01 100644 --- a/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts +++ b/packages/pl-fe/src/queries/utils/make-paginated-response-query-options.ts @@ -1,6 +1,6 @@ import { type InfiniteData, infiniteQueryOptions, type QueryKey } from '@tanstack/react-query'; -import { store } from '@/store'; +import { getClient } from '@/api'; import { PaginatedResponseArray, @@ -23,8 +23,7 @@ const makePaginatedResponseQueryOptions = (...params: T1) => infiniteQueryOptions({ queryKey: typeof queryKey === 'object' ? queryKey : queryKey(...params), - queryFn: ({ pageParam }) => - pageParam.next?.() ?? queryFn(store.getState().auth.client, params), + queryFn: ({ pageParam }) => pageParam.next?.() ?? queryFn(getClient(), params), initialPageParam: { previous: null, next: null, diff --git a/packages/pl-fe/src/reducers/compose.ts b/packages/pl-fe/src/reducers/compose.ts deleted file mode 100644 index 6aca81ec6..000000000 --- a/packages/pl-fe/src/reducers/compose.ts +++ /dev/null @@ -1,888 +0,0 @@ -import { create } from 'mutative'; - -import { INSTANCE_FETCH_SUCCESS, type InstanceAction } from '@/actions/instance'; - -import { - COMPOSE_CHANGE, - COMPOSE_REPLY, - COMPOSE_REPLY_CANCEL, - COMPOSE_QUOTE, - COMPOSE_QUOTE_CANCEL, - COMPOSE_GROUP_POST, - COMPOSE_DIRECT, - COMPOSE_MENTION, - COMPOSE_SUBMIT_REQUEST, - COMPOSE_SUBMIT_SUCCESS, - COMPOSE_SUBMIT_FAIL, - COMPOSE_UPLOAD_REQUEST, - COMPOSE_UPLOAD_SUCCESS, - COMPOSE_UPLOAD_FAIL, - COMPOSE_UPLOAD_UNDO, - COMPOSE_UPLOAD_PROGRESS, - COMPOSE_SUGGESTIONS_CLEAR, - COMPOSE_SUGGESTIONS_READY, - COMPOSE_SUGGESTION_SELECT, - COMPOSE_SUGGESTION_TAGS_UPDATE, - COMPOSE_SPOILERNESS_CHANGE, - COMPOSE_TYPE_CHANGE, - COMPOSE_SPOILER_TEXT_CHANGE, - COMPOSE_VISIBILITY_CHANGE, - COMPOSE_LANGUAGE_CHANGE, - COMPOSE_MODIFIED_LANGUAGE_CHANGE, - COMPOSE_LANGUAGE_ADD, - COMPOSE_LANGUAGE_DELETE, - COMPOSE_ADD_SUGGESTED_LANGUAGE, - COMPOSE_UPLOAD_CHANGE_REQUEST, - COMPOSE_UPLOAD_CHANGE_SUCCESS, - COMPOSE_UPLOAD_CHANGE_FAIL, - COMPOSE_RESET, - COMPOSE_POLL_ADD, - COMPOSE_POLL_REMOVE, - COMPOSE_SCHEDULE_ADD, - COMPOSE_SCHEDULE_SET, - COMPOSE_SCHEDULE_REMOVE, - COMPOSE_POLL_OPTION_ADD, - COMPOSE_POLL_OPTION_CHANGE, - COMPOSE_POLL_OPTION_REMOVE, - COMPOSE_POLL_SETTINGS_CHANGE, - COMPOSE_ADD_TO_MENTIONS, - COMPOSE_REMOVE_FROM_MENTIONS, - COMPOSE_SET_STATUS, - COMPOSE_EVENT_REPLY, - COMPOSE_EDITOR_STATE_SET, - COMPOSE_CHANGE_MEDIA_ORDER, - COMPOSE_ADD_SUGGESTED_QUOTE, - COMPOSE_FEDERATED_CHANGE, - COMPOSE_INTERACTION_POLICY_OPTION_CHANGE, - COMPOSE_CLEAR_LINK_SUGGESTION_CREATE, - COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE, - COMPOSE_PREVIEW_SUCCESS, - COMPOSE_PREVIEW_CANCEL, - COMPOSE_HASHTAG_CASING_SUGGESTION_SET, - COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE, - COMPOSE_REDACTING_OVERWRITE_CHANGE, - COMPOSE_QUOTE_POLICY_OPTION_CHANGE, - COMPOSE_SET_LOCATION, - COMPOSE_SET_SHOW_LOCATION_PICKER, - type ComposeAction, - type ComposeSuggestionSelectAction, -} from '../actions/compose'; -import { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, type EventsAction } from '../actions/events'; -import { ME_FETCH_SUCCESS, ME_PATCH_SUCCESS, type MeAction } from '../actions/me'; -import { FE_NAME } from '../actions/settings'; -import { TIMELINE_DELETE, type TimelineAction } from '../actions/timelines'; -import { unescapeHTML } from '../utils/html'; - -import type { Emoji } from '@/features/emoji'; -import type { Language } from '@/features/preferences'; -import type { Status } from '@/normalizers/status'; -import type { - Account, - CredentialAccount, - Instance, - InteractionPolicy, - Location, - MediaAttachment, - Status as BaseStatus, - Tag, - CreateStatusParams, -} from 'pl-api'; - -const getResetFileKey = () => Math.floor(Math.random() * 0x10000); - -interface ComposePoll { - options: Array; - options_map: Array>; - expires_in: number; - multiple: boolean; - hide_totals: boolean; -} - -const newPoll = (params: Partial = {}): ComposePoll => ({ - options: ['', ''], - options_map: [{}, {}], - expires_in: 24 * 3600, - multiple: false, - hide_totals: false, - ...params, -}); - -interface ClearLinkSuggestion { - key: string; - originalUrl: string; - cleanUrl: string; -} - -interface Compose { - // User-edited text - editorState: string | null; - editorStateMap: Record; - spoilerText: string; - spoilerTextMap: Record; - text: string; - textMap: Record; - - // Non-text content - mediaAttachments: Array; - poll: ComposePoll | null; - location: Location | null; - - // Post settings - contentType: string; - interactionPolicy: InteractionPolicy | null; - quoteApprovalPolicy: CreateStatusParams['quote_approval_policy'] | null; - language: Language | string | null; - localOnly: boolean; - scheduledAt: Date | null; - sensitive: boolean; - visibility: string; - - // References to other posts/groups/users - draftId: string | null; - groupId: string | null; - editedId: string | null; - inReplyToId: string | null; - quoteId: string | null; - to: Array; - parentRebloggedById: string | null; - - // State flags - isChangingUpload: boolean; - isSubmitting: boolean; - isUploading: boolean; - progress: number; - - // Internal - caretPosition: number | null; - idempotencyKey: string; - resetFileKey: number | null; - - // Currently modified language - modifiedLanguage: Language | string | null; - - // Suggestions - approvalRequired: boolean; - clearLinkSuggestion: ClearLinkSuggestion | null; - dismissedClearLinksSuggestions: Array; - dismissedQuotes: Array; - hashtagCasingSuggestion: string | null; - hashtagCasingSuggestionIgnored: boolean | null; - preview: Partial | null; - suggestedLanguage: string | null; - suggestions: Array | Array; - showLocationPicker: boolean; - - // Moderation features - redacting: boolean; - redactingOverwrite: boolean; -} - -const newCompose = (params: Partial = {}): Compose => ({ - editorState: null, - editorStateMap: {}, - spoilerText: '', - spoilerTextMap: {}, - text: '', - textMap: {}, - - mediaAttachments: [], - poll: null, - location: null, - - contentType: 'text/plain', - interactionPolicy: null, - quoteApprovalPolicy: null, - language: null, - localOnly: false, - scheduledAt: null, - sensitive: false, - visibility: 'public', - - draftId: null, - groupId: null, - editedId: null, - inReplyToId: null, - quoteId: null, - to: [], - parentRebloggedById: null, - - isChangingUpload: false, - isSubmitting: false, - isUploading: false, - progress: 0, - - caretPosition: null, - idempotencyKey: '', - resetFileKey: null, - - modifiedLanguage: null, - - approvalRequired: false, - clearLinkSuggestion: null, - dismissedClearLinksSuggestions: [], - dismissedQuotes: [], - hashtagCasingSuggestion: null, - hashtagCasingSuggestionIgnored: null, - preview: null, - suggestedLanguage: null, - suggestions: [], - showLocationPicker: false, - - redacting: false, - redactingOverwrite: false, - - ...params, -}); - -type State = { - default: Compose; - [key: string]: Compose; -}; - -const statusToTextMentions = ( - status: Pick, - account: Pick, -) => { - const author = status.account.acct; - const mentions = status.mentions.map((m) => m.acct) || []; - - return [...new Set([author, ...mentions].filter((acct) => acct !== account.acct))] - .map((m) => `@${m} `) - .join(''); -}; - -const statusToMentionsArray = ( - status: Pick, - account: Pick, - rebloggedBy?: Pick, -) => { - const author = status.account.acct; - const mentions = status.mentions.map((m) => m.acct) || []; - - return [ - ...new Set( - [author, ...(rebloggedBy ? [rebloggedBy.acct] : []), ...mentions].filter( - (acct) => acct !== account.acct, - ), - ), - ]; -}; - -const statusToMentionsAccountIdsArray = ( - status: Pick, - account: Pick, - parentRebloggedBy?: string | null, -) => { - const mentions = status.mentions.map((m) => m.id); - - return [ - ...new Set( - [status.account.id, ...(parentRebloggedBy ? [parentRebloggedBy] : []), ...mentions].filter( - (id) => id !== account.id, - ), - ), - ]; -}; - -const appendMedia = (compose: Compose, media: MediaAttachment, defaultSensitive?: boolean) => { - const prevSize = compose.mediaAttachments.length; - - compose.mediaAttachments.push(media); - compose.isUploading = false; - compose.resetFileKey = Math.floor(Math.random() * 0x10000); - - if (prevSize === 0 && (defaultSensitive || compose.sensitive)) { - compose.sensitive = true; - } -}; - -const removeMedia = (compose: Compose, mediaId: string) => { - const prevSize = compose.mediaAttachments.length; - - compose.mediaAttachments = compose.mediaAttachments.filter((item) => item.id !== mediaId); - - if (prevSize === 1) { - compose.sensitive = false; - } -}; - -const insertSuggestion = ( - compose: Compose, - position: number, - token: string | null, - completion: string, - path: ComposeSuggestionSelectAction['path'], -) => { - const updateText = (oldText?: string) => - `${oldText?.slice(0, position)}${completion} ${oldText?.slice(position + (token?.length ?? 0))}`; - if (path[0] === 'spoiler_text') { - compose.spoilerText = updateText(compose.spoilerText); - } else if (compose.poll) { - compose.poll.options[path[2]] = updateText(compose.poll.options[path[2]]); - } - compose.suggestions = []; -}; - -const updateSuggestionTags = (compose: Compose, token: string, tags: Tag[]) => { - const prefix = token.slice(1); - - compose.suggestions = tags - .filter((tag) => tag.name.toLowerCase().startsWith(prefix.toLowerCase())) - .slice(0, 4) - .map((tag) => '#' + tag.name); -}; - -const privacyPreference = ( - a: string, - b: string, - list_id: number | null, - conversationScope = false, -) => { - if (['private', 'subscribers'].includes(a) && conversationScope) return 'conversation'; - - const order = ['public', 'unlisted', 'mutuals_only', 'private', 'direct', 'local']; - - if (a === 'group') return a; - if (a === 'list' && list_id !== null) return `list:${list_id}`; - - return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; -}; - -const domParser = new DOMParser(); - -const expandMentions = (status: Pick) => { - const fragment = domParser.parseFromString(status.content, 'text/html').documentElement; - - status.mentions.forEach((mention) => { - const node = fragment.querySelector(`a[href="${mention.url}"]`); - if (node) node.textContent = `@${mention.acct}`; - }); - - return fragment.innerHTML; -}; - -const getExplicitMentions = (me: string, status: Pick) => { - const fragment = domParser.parseFromString(status.content, 'text/html').documentElement; - - const mentions = status.mentions - .filter((mention) => !(fragment.querySelector(`a[href="${mention.url}"]`) ?? mention.id === me)) - .map((m) => m.acct); - - return [...new Set(mentions)]; -}; - -const importAccount = (compose: Compose, account: CredentialAccount) => { - const settings = account.settings_store?.[FE_NAME]; - - if (!settings) return; - - if (settings.defaultPrivacy) compose.visibility = settings.defaultPrivacy; - if (settings.defaultContentType) compose.contentType = settings.defaultContentType; -}; - -// const updateSetting = (compose: Compose, path: string[], value: string) => { -// const pathString = path.join(','); -// switch (pathString) { -// case 'defaultPrivacy': -// return compose.set('privacy', value); -// case 'defaultContentType': -// return compose.set('content_type', value); -// default: -// return compose; -// } -// }; - -const updateDefaultContentType = (compose: Compose, instance: Instance) => { - const postFormats = instance.pleroma.metadata.post_formats; - - compose.contentType = - postFormats.includes(compose.contentType) || - (postFormats.includes('text/markdown') && compose.contentType === 'wysiwyg') - ? compose.contentType - : postFormats.includes('text/markdown') - ? 'text/markdown' - : postFormats[0]; -}; - -const updateCompose = (state: State, key: string, updater: (compose: Compose) => void) => - create(state, (draft) => { - draft[key] = - draft[key] || - create(draft.default, (draft) => { - draft.idempotencyKey = crypto.randomUUID(); - }); - updater(draft[key]); - }); -// state.update(key, state.get('default')!, updater); - -const initialState: State = { - default: newCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), -}; - -const compose = ( - state = initialState, - action: ComposeAction | EventsAction | InstanceAction | MeAction | TimelineAction, -): State => { - switch (action.type) { - case COMPOSE_TYPE_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.contentType = action.value; - }); - case COMPOSE_SPOILERNESS_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.sensitive = !compose.sensitive; - }); - case COMPOSE_SPOILER_TEXT_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.modifiedLanguage || compose.modifiedLanguage === compose.language) { - compose.spoilerText = action.text; - } else if (compose.modifiedLanguage) { - compose.spoilerTextMap[compose.modifiedLanguage] = action.text; - } - }); - case COMPOSE_VISIBILITY_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.visibility = action.value; - }); - case COMPOSE_LANGUAGE_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.language = action.value; - compose.modifiedLanguage = action.value; - }); - case COMPOSE_MODIFIED_LANGUAGE_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.modifiedLanguage = action.value; - }); - case COMPOSE_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.text = action.text; - }); - case COMPOSE_REPLY: - return updateCompose(state, action.composeId, (compose) => { - const defaultCompose = state.default; - - const mentions = action.explicitAddressing - ? statusToMentionsArray(action.status, action.account, action.rebloggedBy) - : []; - - compose.groupId = action.status.group_id; - compose.inReplyToId = action.status.id; - compose.to = mentions; - compose.parentRebloggedById = action.rebloggedBy?.id ?? null; - compose.text = !action.explicitAddressing - ? statusToTextMentions(action.status, action.account) - : ''; - compose.visibility = privacyPreference( - action.status.visibility, - defaultCompose.visibility, - action.status.list_id, - action.conversationScope, - ); - compose.localOnly = action.status.local_only === true; - compose.caretPosition = null; - compose.contentType = defaultCompose.contentType; - compose.approvalRequired = action.approvalRequired ?? false; - if (action.preserveSpoilers && action.status.spoiler_text) { - compose.sensitive = true; - compose.spoilerText = action.status.spoiler_text; - } - }); - case COMPOSE_EVENT_REPLY: - return updateCompose(state, action.composeId, (compose) => { - compose.inReplyToId = action.status.id; - compose.to = statusToMentionsArray(action.status, action.account); - }); - case COMPOSE_QUOTE: - return updateCompose(state, 'compose-modal', (compose) => { - const author = action.status.account.acct; - const defaultCompose = state.default; - - compose.quoteId = action.status.id; - compose.to = [author]; - compose.parentRebloggedById = null; - compose.text = ''; - compose.visibility = privacyPreference( - action.status.visibility, - defaultCompose.visibility, - action.status.list_id, - ); - compose.caretPosition = null; - compose.contentType = defaultCompose.contentType; - compose.spoilerText = ''; - compose.approvalRequired = action.approvalRequired ?? false; - - if (action.status.visibility === 'group') { - compose.groupId = action.status.group_id; - compose.visibility = 'group'; - } - }); - case COMPOSE_SUBMIT_REQUEST: - return updateCompose(state, action.composeId, (compose) => { - compose.isSubmitting = true; - }); - case COMPOSE_UPLOAD_CHANGE_REQUEST: - return updateCompose(state, action.composeId, (compose) => { - compose.isChangingUpload = true; - }); - case COMPOSE_REPLY_CANCEL: - case COMPOSE_RESET: - case COMPOSE_SUBMIT_SUCCESS: - return create(state, (draft) => { - draft[action.composeId] = create(state.default, (draft) => ({ - ...draft, - idempotencyKey: crypto.randomUUID(), - in_reply_to_id: action.composeId.startsWith('reply:') ? action.composeId.slice(6) : null, - ...(action.composeId.startsWith('group:') - ? { - visibility: 'group', - group_id: action.composeId.slice(6), - } - : undefined), - })); - }); - case COMPOSE_SUBMIT_FAIL: - return updateCompose(state, action.composeId, (compose) => { - compose.isSubmitting = false; - }); - case COMPOSE_UPLOAD_CHANGE_FAIL: - return updateCompose(state, action.composeId, (compose) => { - compose.isChangingUpload = false; - }); - case COMPOSE_UPLOAD_REQUEST: - return updateCompose(state, action.composeId, (compose) => { - compose.isUploading = true; - }); - case COMPOSE_UPLOAD_SUCCESS: - return updateCompose(state, action.composeId, (compose) => { - appendMedia(compose, action.media, state.default.sensitive); - }); - case COMPOSE_UPLOAD_FAIL: - return updateCompose(state, action.composeId, (compose) => { - compose.isUploading = false; - }); - case COMPOSE_UPLOAD_UNDO: - return updateCompose(state, action.composeId, (compose) => { - removeMedia(compose, action.mediaId); - }); - case COMPOSE_UPLOAD_PROGRESS: - return updateCompose(state, action.composeId, (compose) => { - compose.progress = Math.round((action.loaded / action.total) * 100); - }); - case COMPOSE_MENTION: - return updateCompose(state, 'compose-modal', (compose) => { - compose.text = [compose.text.trim(), `@${action.account.acct} `] - .filter((str) => str.length !== 0) - .join(' '); - compose.caretPosition = null; - }); - case COMPOSE_DIRECT: - return updateCompose(state, 'compose-modal', (compose) => { - compose.text = [compose.text.trim(), `@${action.account.acct} `] - .filter((str) => str.length !== 0) - .join(' '); - compose.visibility = 'direct'; - compose.caretPosition = null; - }); - case COMPOSE_GROUP_POST: - return updateCompose(state, action.composeId, (compose) => { - compose.visibility = 'group'; - compose.groupId = action.groupId; - compose.caretPosition = null; - }); - case COMPOSE_SUGGESTIONS_CLEAR: - return updateCompose(state, action.composeId, (compose) => { - compose.suggestions = []; - }); - case COMPOSE_SUGGESTIONS_READY: - return updateCompose(state, action.composeId, (compose) => { - compose.suggestions = action.accounts - ? action.accounts.map((item) => item.id) - : (action.emojis ?? []); - }); - case COMPOSE_SUGGESTION_SELECT: - return updateCompose(state, action.composeId, (compose) => { - insertSuggestion(compose, action.position, action.token, action.completion, action.path); - }); - case COMPOSE_SUGGESTION_TAGS_UPDATE: - return updateCompose(state, action.composeId, (compose) => { - updateSuggestionTags(compose, action.token, action.tags); - }); - case TIMELINE_DELETE: - return updateCompose(state, 'compose-modal', (compose) => { - if (action.statusId === compose.inReplyToId) { - compose.inReplyToId = null; - } - if (action.statusId === compose.quoteId) { - compose.quoteId = null; - } - }); - case COMPOSE_UPLOAD_CHANGE_SUCCESS: - return updateCompose(state, action.composeId, (compose) => { - compose.isChangingUpload = false; - - compose.mediaAttachments = compose.mediaAttachments.map((item) => { - if (item.id === action.media.id) { - return action.media; - } - - return item; - }); - }); - case COMPOSE_SET_STATUS: - return updateCompose(state, 'compose-modal', (compose) => { - const mentions = action.explicitAddressing - ? getExplicitMentions(action.status.account.id, action.status) - : []; - if (!action.withRedraft && !action.draftId) { - compose.editedId = action.status.id; - } - compose.text = action.rawText || unescapeHTML(expandMentions(action.status)); - compose.to = mentions; - compose.parentRebloggedById = null; - compose.inReplyToId = action.status.in_reply_to_id; - compose.visibility = action.status.visibility; - compose.caretPosition = null; - const contentType = - action.contentType === 'text/markdown' && state.default.contentType === 'wysiwyg' - ? 'wysiwyg' - : action.contentType || 'text/plain'; - compose.contentType = contentType; - compose.quoteId = action.status.quote_id; - compose.groupId = action.status.group_id; - compose.language = action.status.language; - - compose.mediaAttachments = action.status.media_attachments; - compose.sensitive = action.status.sensitive; - - compose.redacting = action.redacting ?? false; - - if (action.status.spoiler_text.length > 0) { - compose.spoilerText = action.status.spoiler_text; - } else { - compose.spoilerText = ''; - } - - if (action.poll) { - compose.poll = newPoll({ - options: action.poll.options.map(({ title }) => title), - multiple: action.poll.multiple, - expires_in: 24 * 3600, - }); - } - - if (action.draftId) { - compose.draftId = action.draftId; - } - - if (action.editorState) { - compose.editorState = action.editorState; - } - }); - case COMPOSE_POLL_ADD: - return updateCompose(state, action.composeId, (compose) => { - compose.poll = newPoll(); - }); - case COMPOSE_POLL_REMOVE: - return updateCompose(state, action.composeId, (compose) => { - compose.poll = null; - }); - case COMPOSE_SCHEDULE_ADD: - return updateCompose(state, action.composeId, (compose) => { - compose.scheduledAt = new Date(Date.now() + 10 * 60 * 1000); - }); - case COMPOSE_SCHEDULE_SET: - return updateCompose(state, action.composeId, (compose) => { - compose.scheduledAt = action.date; - }); - case COMPOSE_SCHEDULE_REMOVE: - return updateCompose(state, action.composeId, (compose) => { - compose.scheduledAt = null; - }); - case COMPOSE_POLL_OPTION_ADD: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.poll) return; - compose.poll.options.push(action.title); - compose.poll.options_map.push( - Object.fromEntries(Object.entries(compose.textMap).map((key) => [key, action.title])), - ); - }); - case COMPOSE_POLL_OPTION_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.poll) return; - if (!compose.modifiedLanguage || compose.modifiedLanguage === compose.language) { - compose.poll.options[action.index] = action.title; - if (compose.modifiedLanguage) - compose.poll.options_map[action.index][compose.modifiedLanguage] = action.title; - } - }); - case COMPOSE_POLL_OPTION_REMOVE: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.poll) return; - compose.poll.options = compose.poll.options.filter((_, index) => index !== action.index); - compose.poll.options_map = compose.poll.options_map.filter( - (_, index) => index !== action.index, - ); - }); - case COMPOSE_POLL_SETTINGS_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.poll) return null; - if (action.expiresIn) { - compose.poll.expires_in = action.expiresIn; - } - if (typeof action.isMultiple === 'boolean') { - compose.poll.multiple = action.isMultiple; - } - }); - case COMPOSE_ADD_TO_MENTIONS: - return updateCompose(state, action.composeId, (compose) => { - compose.to = [...new Set([...compose.to, action.account])]; - }); - case COMPOSE_REMOVE_FROM_MENTIONS: - return updateCompose(state, action.composeId, (compose) => { - compose.to = compose.to.filter((acct) => acct !== action.account); - }); - case ME_FETCH_SUCCESS: - case ME_PATCH_SUCCESS: - return updateCompose(state, 'default', (compose) => { - importAccount(compose, action.me); - }); - // case SETTING_CHANGE: - // return updateCompose(state, 'default', compose => updateSetting(compose, action.path, action.value)); - case COMPOSE_EDITOR_STATE_SET: - return updateCompose(state, action.composeId, (compose) => { - if (!compose.modifiedLanguage || compose.modifiedLanguage === compose.language) { - compose.editorState = action.editorState as string; - compose.text = action.text as string; - } else if (compose.modifiedLanguage) { - compose.editorStateMap[compose.modifiedLanguage] = action.editorState as string; - compose.textMap[compose.modifiedLanguage] = action.text as string; - } - }); - case EVENT_COMPOSE_CANCEL: - return updateCompose(state, 'event-compose-modal', (compose) => { - compose.text = ''; - }); - case EVENT_FORM_SET: - return updateCompose(state, action.composeId, (compose) => { - compose.text = action.text; - }); - case COMPOSE_CHANGE_MEDIA_ORDER: - return updateCompose(state, action.composeId, (compose) => { - const indexA = compose.mediaAttachments.findIndex((x) => x.id === action.a); - const indexB = compose.mediaAttachments.findIndex((x) => x.id === action.b); - - const item = compose.mediaAttachments.splice(indexA, 1)[0]; - compose.mediaAttachments.splice(indexB, 0, item); - }); - case COMPOSE_ADD_SUGGESTED_QUOTE: - return updateCompose(state, action.composeId, (compose) => { - compose.quoteId = action.quoteId; - }); - case COMPOSE_ADD_SUGGESTED_LANGUAGE: - return updateCompose(state, action.composeId, (compose) => { - compose.suggestedLanguage = action.language; - }); - case COMPOSE_LANGUAGE_ADD: - return updateCompose(state, action.composeId, (compose) => { - compose.editorStateMap[action.value] = compose.editorState; - compose.textMap[action.value] = compose.text; - compose.spoilerTextMap[action.value] = compose.spoilerText; - if (compose.poll) - compose.poll.options_map.forEach( - (option, key) => (option[action.value] = compose.poll!.options[key]), - ); - }); - case COMPOSE_LANGUAGE_DELETE: - return updateCompose(state, action.composeId, (compose) => { - delete compose.editorStateMap[action.value]; - delete compose.textMap[action.value]; - delete compose.spoilerTextMap[action.value]; - }); - case COMPOSE_QUOTE_CANCEL: - return updateCompose(state, action.composeId, (compose) => { - if (compose.quoteId) compose.dismissedQuotes.push(compose.quoteId); - compose.quoteId = null; - }); - case COMPOSE_FEDERATED_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.localOnly = !compose.localOnly; - }); - case COMPOSE_INTERACTION_POLICY_OPTION_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.interactionPolicy ??= JSON.parse(JSON.stringify(action.initial))!; - - compose.interactionPolicy = create( - compose.interactionPolicy ?? action.initial, - (interactionPolicy) => { - interactionPolicy[action.policy][action.rule] = action.value; - interactionPolicy[action.policy][ - action.rule === 'always' ? 'with_approval' : 'always' - ] = interactionPolicy[action.policy][ - action.rule === 'always' ? 'with_approval' : 'always' - ].filter((rule) => !action.value.includes(rule as any)); - }, - ); - }); - case COMPOSE_QUOTE_POLICY_OPTION_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.quoteApprovalPolicy = action.value; - }); - case INSTANCE_FETCH_SUCCESS: - return updateCompose(state, 'default', (compose) => { - updateDefaultContentType(compose, action.instance); - }); - case COMPOSE_CLEAR_LINK_SUGGESTION_CREATE: - return updateCompose(state, action.composeId, (compose) => { - compose.clearLinkSuggestion = action.suggestion; - }); - case COMPOSE_CLEAR_LINK_SUGGESTION_IGNORE: - return updateCompose(state, action.composeId, (compose) => { - if (compose.clearLinkSuggestion?.key === action.key) { - compose.clearLinkSuggestion = null; - } - compose.dismissedClearLinksSuggestions.push(action.key); - }); - case COMPOSE_PREVIEW_SUCCESS: - return updateCompose(state, action.composeId, (compose) => { - compose.preview = action.status; - }); - case COMPOSE_PREVIEW_CANCEL: - return updateCompose(state, action.composeId, (compose) => { - compose.preview = null; - }); - case COMPOSE_HASHTAG_CASING_SUGGESTION_SET: - return updateCompose(state, action.composeId, (compose) => { - compose.hashtagCasingSuggestion = action.suggestion; - }); - case COMPOSE_HASHTAG_CASING_SUGGESTION_IGNORE: - return updateCompose(state, action.composeId, (compose) => { - compose.hashtagCasingSuggestion = null; - compose.hashtagCasingSuggestionIgnored = true; - }); - case COMPOSE_REDACTING_OVERWRITE_CHANGE: - return updateCompose(state, action.composeId, (compose) => { - compose.redactingOverwrite = action.value; - }); - case COMPOSE_SET_LOCATION: - return updateCompose(state, action.composeId, (compose) => { - compose.location = action.location; - }); - case COMPOSE_SET_SHOW_LOCATION_PICKER: - return updateCompose(state, action.composeId, (compose) => { - compose.showLocationPicker = action.showLocation; - if (!action.showLocation) { - compose.location = null; - } - }); - default: - return state; - } -}; - -export { - type Compose, - type ClearLinkSuggestion, - statusToMentionsAccountIdsArray, - initialState, - compose as default, -}; diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index 8779c276c..11b628f8f 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -5,7 +5,6 @@ import * as BuildConfig from '@/build-config'; import admin from './admin'; import auth from './auth'; -import compose from './compose'; import filters from './filters'; import frontendConfig from './frontend-config'; import instance from './instance'; @@ -18,7 +17,6 @@ import timelines from './timelines'; const reducers = { admin, auth, - compose, filters, frontendConfig, instance, diff --git a/packages/pl-fe/src/stores/compose.ts b/packages/pl-fe/src/stores/compose.ts new file mode 100644 index 000000000..01e6f1bfc --- /dev/null +++ b/packages/pl-fe/src/stores/compose.ts @@ -0,0 +1,1038 @@ +import { useCallback } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +import { uploadFile, updateMedia } from '@/actions/media'; +import { saveSettings } from '@/actions/settings'; +import { FE_NAME } from '@/actions/settings'; +import { createStatus } from '@/actions/statuses'; +import { getClient } from '@/api'; +import { isNativeEmoji } from '@/features/emoji'; +import { useAppDispatch } from '@/hooks/use-app-dispatch'; +import { useClient } from '@/hooks/use-client'; +import { useFeatures } from '@/hooks/use-features'; +import { useInstance } from '@/hooks/use-instance'; +import { selectAccount, selectOwnAccount } from '@/queries/accounts/selectors'; +import { queryClient } from '@/queries/client'; +import { cancelDraftStatus } from '@/queries/statuses/use-draft-statuses'; +import { useModalsActions, useModalsStore } from '@/stores/modals'; +import { useSettings, useSettingsStore } from '@/stores/settings'; +import toast from '@/toast'; + +import type { AutoSuggestion } from '@/components/autosuggest-input'; +import type { Language } from '@/features/preferences'; +import type { Status } from '@/normalizers/status'; +import type { AppDispatch, RootState } from '@/store'; +import type { LinkOptions } from '@tanstack/react-router'; +import type { + Account, + CreateStatusParams, + Group, + MediaAttachment, + Status as BaseStatus, + Poll, + InteractionPolicy, + UpdateMediaParams, + Location, + EditStatusParams, + CredentialAccount, + Instance, + StatusSource, +} from 'pl-api'; + +const messages = defineMessages({ + scheduleError: { + id: 'compose.invalid_schedule', + defaultMessage: 'You must schedule a post at least 5 minutes out.', + }, + success: { id: 'compose.submit_success', defaultMessage: 'Your post was sent!' }, + editSuccess: { id: 'compose.edit_success', defaultMessage: 'Your post was edited' }, + redactSuccess: { id: 'compose.redact_success', defaultMessage: 'The post was redacted' }, + scheduledSuccess: { id: 'compose.scheduled_success', defaultMessage: 'Your post was scheduled' }, + uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, + uploadErrorPoll: { + id: 'upload_error.poll', + defaultMessage: 'File upload not allowed with polls.', + }, + view: { id: 'toast.view', defaultMessage: 'View' }, + replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, + replyMessage: { + id: 'confirmations.reply.message', + defaultMessage: + 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?', + }, +}); + +const getResetFileKey = () => Math.floor(Math.random() * 0x10000); + +interface ComposePoll { + options: Array; + options_map: Array>; + expires_in: number; + multiple: boolean; + hide_totals: boolean; +} + +interface ClearLinkSuggestion { + key: string; + originalUrl: string; + cleanUrl: string; +} + +interface Compose { + // User-edited text + editorState: string | null; + editorStateMap: Record; + spoilerText: string; + spoilerTextMap: Record; + text: string; + textMap: Record; + + // Non-text content + mediaAttachments: Array; + poll: ComposePoll | null; + location: Location | null; + + // Post settings + contentType: string; + interactionPolicy: InteractionPolicy | null; + quoteApprovalPolicy: CreateStatusParams['quote_approval_policy'] | null; + language: Language | string | null; + localOnly: boolean; + scheduledAt: Date | null; + sensitive: boolean; + visibility: string; + + // References to other posts/groups/users + draftId: string | null; + groupId: string | null; + editedId: string | null; + inReplyToId: string | null; + quoteId: string | null; + to: Array; + parentRebloggedById: string | null; + + // State flags + isChangingUpload: boolean; + isSubmitting: boolean; + isUploading: boolean; + progress: number; + + // Internal + caretPosition: number | null; + idempotencyKey: string; + resetFileKey: number | null; + + // Currently modified language + modifiedLanguage: Language | string | null; + + // Suggestions + approvalRequired: boolean; + clearLinkSuggestion: ClearLinkSuggestion | null; + dismissedClearLinksSuggestions: Array; + dismissedQuotes: Array; + hashtagCasingSuggestion: string | null; + hashtagCasingSuggestionIgnored: boolean | null; + preview: Partial | null; + suggestedLanguage: string | null; + showLocationPicker: boolean; + + // Moderation features + redacting: boolean; + redactingOverwrite: boolean; +} + +const newCompose = (params: Partial = {}): Compose => ({ + editorState: null, + editorStateMap: {}, + spoilerText: '', + spoilerTextMap: {}, + text: '', + textMap: {}, + + mediaAttachments: [], + poll: null, + location: null, + + contentType: 'text/plain', + interactionPolicy: null, + quoteApprovalPolicy: null, + language: null, + localOnly: false, + scheduledAt: null, + sensitive: false, + visibility: 'public', + + draftId: null, + groupId: null, + editedId: null, + inReplyToId: null, + quoteId: null, + to: [], + parentRebloggedById: null, + + isChangingUpload: false, + isSubmitting: false, + isUploading: false, + progress: 0, + + caretPosition: null, + idempotencyKey: '', + resetFileKey: null, + + modifiedLanguage: null, + + approvalRequired: false, + clearLinkSuggestion: null, + dismissedClearLinksSuggestions: [], + dismissedQuotes: [], + hashtagCasingSuggestion: null, + hashtagCasingSuggestionIgnored: null, + preview: null, + suggestedLanguage: null, + showLocationPicker: false, + + redacting: false, + redactingOverwrite: false, + + ...params, +}); + +const newPoll = (params: Partial = {}): ComposePoll => ({ + options: ['', ''], + options_map: [{}, {}], + expires_in: 24 * 3600, + multiple: false, + hide_totals: false, + ...params, +}); + +const statusToTextMentions = ( + status: Pick, + account: Pick, +) => { + const author = status.account.acct; + const mentions = status.mentions.map((m) => m.acct); + + return [...new Set([author, ...mentions].filter((acct) => acct !== account.acct))] + .map((m) => `@${m} `) + .join(''); +}; + +const statusToMentionsArray = ( + status: Pick, + account: Pick, + rebloggedBy?: Pick, +) => { + const author = status.account.acct; + const mentions = status.mentions.map((m) => m.acct); + + return [ + ...new Set( + [author, ...(rebloggedBy ? [rebloggedBy.acct] : []), ...mentions].filter( + (acct) => acct !== account.acct, + ), + ), + ]; +}; + +const statusToMentionsAccountIdsArray = ( + status: Pick, + account: Pick, + parentRebloggedBy?: string | null, +) => { + const mentions = status.mentions.map((m) => m.id); + + return [ + ...new Set( + [status.account.id, ...(parentRebloggedBy ? [parentRebloggedBy] : []), ...mentions].filter( + (id) => id !== account.id, + ), + ), + ]; +}; + +const privacyPreference = ( + a: string, + b: string, + list_id: number | null, + conversationScope = false, +) => { + if (['private', 'subscribers'].includes(a) && conversationScope) return 'conversation'; + + const order = ['public', 'unlisted', 'mutuals_only', 'private', 'direct', 'local']; + + if (a === 'group') return a; + if (a === 'list' && list_id !== null) return `list:${list_id}`; + + return order[Math.max(order.indexOf(a), order.indexOf(b), 0)]; +}; + +const domParser = new DOMParser(); + +const getExplicitMentions = (me: string, status: Pick) => { + const fragment = domParser.parseFromString(status.content, 'text/html').documentElement; + + const mentions = status.mentions + .filter((mention) => !(fragment.querySelector(`a[href="${mention.url}"]`) ?? mention.id === me)) + .map((m) => m.acct); + + return [...new Set(mentions)]; +}; + +const appendMedia = (compose: Compose, media: MediaAttachment) => { + const prevSize = compose.mediaAttachments.length; + + compose.mediaAttachments.push(media); + compose.isUploading = false; + compose.resetFileKey = Math.floor(Math.random() * 0x10000); + + if (prevSize === 0 && compose.sensitive) { + compose.sensitive = true; + } +}; + +interface ComposeState { + default: Compose; + composers: Record; +} + +interface ComposeActions { + updateCompose: (composeId: string, updater: (draft: Compose) => void) => void; + updateAllCompose: (updater: (draft: Compose) => void) => void; + getCompose: (composeId: string) => Compose; + + setComposeToStatus: ( + status: Pick< + Status, + | 'id' + | 'account' + | 'content' + | 'group_id' + | 'in_reply_to_id' + | 'language' + | 'media_attachments' + | 'mentions' + | 'quote_id' + | 'sensitive' + | 'spoiler_text' + | 'visibility' + >, + poll: Poll | null | undefined, + source: Pick, + withRedraft?: boolean, + draftId?: string | null, + editorState?: string | null, + redacting?: boolean, + ) => void; + replyCompose: ( + status: Pick< + Status, + | 'id' + | 'account' + | 'group_id' + | 'list_id' + | 'local_only' + | 'mentions' + | 'spoiler_text' + | 'visibility' + >, + rebloggedBy?: Pick, + approvalRequired?: boolean, + ) => void; + quoteCompose: ( + status: Pick, + approvalRequired?: boolean, + ) => void; + mentionCompose: (account: Pick) => void; + directCompose: (account: Pick) => void; + groupComposeModal: (group: Pick) => void; + openComposeWithText: (composeId: string, text?: string) => void; + eventDiscussionCompose: ( + composeId: string, + status: Pick, + ) => void; + resetCompose: (composeId?: string) => void; + selectComposeSuggestion: ( + composeId: string, + position: number, + token: string | null, + suggestion: AutoSuggestion, + path: ['spoiler_text'] | ['poll', 'options', number], + ) => void; + + importDefaultSettings: (account: CredentialAccount) => void; + importDefaultContentType: (instance: Instance) => void; + handleTimelineDelete: (statusId: string) => void; +} + +type ComposeStore = ComposeState & { actions: ComposeActions }; + +let lazyStore: { dispatch: AppDispatch; getState: () => RootState }; +import('@/store').then(({ store }) => (lazyStore = store)).catch(() => {}); + +const useComposeStore = create()( + mutative( + (set, get) => ({ + default: newCompose({ idempotencyKey: crypto.randomUUID(), resetFileKey: getResetFileKey() }), + composers: {}, + + actions: { + updateCompose: (composeId, updater) => { + set((state) => { + if (!state.composers[composeId]) { + state.composers[composeId] = { + ...state.default, + idempotencyKey: crypto.randomUUID(), + }; + } + updater(state.composers[composeId]); + }); + }, + + updateAllCompose: (updater) => { + set((state) => { + Object.values(state.composers).forEach((compose) => { + updater(compose); + }); + }); + }, + + getCompose: (composeId) => get().composers[composeId] ?? get().default, + + setComposeToStatus: ( + status, + poll, + source, + withRedraft = false, + draftId = null, + editorState = null, + redacting = false, + ) => { + const { features } = getClient(lazyStore.getState); + const explicitAddressing = + features.createStatusExplicitAddressing && + !useSettingsStore.getState().settings.forceImplicitAddressing; + + set((state) => { + state.composers['compose-modal'] = { + ...state.default, + idempotencyKey: crypto.randomUUID(), + }; + + const compose = state.composers['compose-modal']; + const mentions = explicitAddressing + ? getExplicitMentions(status.account.id, status) + : []; + if (!withRedraft && !draftId) { + compose.editedId = status.id; + } + compose.text = source.text; + compose.to = mentions; + compose.parentRebloggedById = null; + compose.inReplyToId = status.in_reply_to_id; + compose.visibility = status.visibility; + compose.caretPosition = null; + const contentType = + source.content_type === 'text/markdown' && state.default.contentType === 'wysiwyg' + ? 'wysiwyg' + : source.content_type || 'text/plain'; + compose.contentType = contentType; + compose.quoteId = status.quote_id; + compose.groupId = status.group_id; + compose.language = status.language; + + compose.mediaAttachments = status.media_attachments; + compose.sensitive = status.sensitive; + + compose.redacting = redacting ?? false; + + compose.spoilerText = source.spoiler_text; + + if (poll) { + compose.poll = newPoll({ + options: poll.options.map(({ title }) => title), + multiple: poll.multiple, + expires_in: 24 * 3600, + }); + } + + if (draftId) { + compose.draftId = draftId; + } + + if (editorState) { + compose.editorState = editorState; + } + }); + }, + + replyCompose: (status, rebloggedBy, approvalRequired) => { + const state = lazyStore.getState(); + const { features } = getClient(lazyStore.getState); + const { forceImplicitAddressing, preserveSpoilers } = + useSettingsStore.getState().settings; + const explicitAddressing = + features.createStatusExplicitAddressing && !forceImplicitAddressing; + const account = selectOwnAccount(state); + + if (!account) return; + + set((draft) => { + if (!draft.composers['compose-modal']) { + draft.composers['compose-modal'] = { + ...draft.default, + idempotencyKey: crypto.randomUUID(), + }; + } + const compose = draft.composers['compose-modal']; + + const mentions = explicitAddressing + ? statusToMentionsArray(status, account, rebloggedBy) + : []; + + compose.groupId = status.group_id; + compose.inReplyToId = status.id; + compose.to = mentions; + compose.parentRebloggedById = rebloggedBy?.id ?? null; + compose.text = !explicitAddressing ? statusToTextMentions(status, account) : ''; + compose.visibility = privacyPreference( + status.visibility, + draft.default.visibility, + status.list_id, + features.createStatusConversationScope, + ); + compose.localOnly = status.local_only === true; + compose.caretPosition = null; + compose.contentType = draft.default.contentType; + compose.approvalRequired = approvalRequired ?? false; + if (preserveSpoilers && status.spoiler_text) { + compose.sensitive = true; + compose.spoilerText = status.spoiler_text; + } + }); + + useModalsStore.getState().actions.openModal('COMPOSE'); + }, + + quoteCompose: (status, approvalRequired) => { + set((draft) => { + if (!draft.composers['compose-modal']) { + draft.composers['compose-modal'] = { + ...draft.default, + idempotencyKey: crypto.randomUUID(), + }; + } + const compose = draft.composers['compose-modal']; + + const author = status.account.acct; + + compose.quoteId = status.id; + compose.to = [author]; + compose.parentRebloggedById = null; + compose.text = ''; + compose.visibility = privacyPreference( + status.visibility, + draft.default.visibility, + status.list_id, + ); + compose.caretPosition = null; + compose.contentType = draft.default.contentType; + compose.spoilerText = ''; + compose.approvalRequired = approvalRequired ?? false; + + if (status.visibility === 'group') { + compose.groupId = status.group_id; + compose.visibility = 'group'; + } + }); + + useModalsStore.getState().actions.openModal('COMPOSE'); + }, + + mentionCompose: (account) => { + if (!lazyStore.getState().me) return; + + get().actions.updateCompose('compose-modal', (compose) => { + compose.text = [compose.text.trim(), `@${account.acct} `] + .filter((str) => str.length !== 0) + .join(' '); + compose.caretPosition = null; + }); + useModalsStore.getState().actions.openModal('COMPOSE'); + }, + + directCompose: (account) => { + get().actions.updateCompose('compose-modal', (compose) => { + compose.text = [compose.text.trim(), `@${account.acct} `] + .filter((str) => str.length !== 0) + .join(' '); + compose.visibility = 'direct'; + compose.caretPosition = null; + }); + useModalsStore.getState().actions.openModal('COMPOSE'); + }, + + groupComposeModal: (group) => { + const composeId = `group:${group.id}`; + get().actions.updateCompose(composeId, (draft) => { + draft.visibility = 'group'; + draft.groupId = group.id; + draft.caretPosition = null; + }); + useModalsStore.getState().actions.openModal('COMPOSE', { composeId }); + }, + + openComposeWithText: (composeId, text = '') => { + set((state) => { + state.composers[composeId] = { + ...state.default, + idempotencyKey: crypto.randomUUID(), + resetFileKey: getResetFileKey(), + ...(composeId.startsWith('reply:') ? { inReplyToId: composeId.slice(6) } : undefined), + ...(composeId.startsWith('group:') + ? { visibility: 'group', groupId: composeId.slice(6) } + : undefined), + text, + }; + }); + useModalsStore.getState().actions.openModal('COMPOSE'); + }, + + eventDiscussionCompose: (composeId, status) => { + const state = lazyStore.getState(); + const account = selectOwnAccount(state); + + if (!account) return; + + get().actions.updateCompose(composeId, (compose) => { + compose.inReplyToId = status.id; + compose.to = statusToMentionsArray(status, account); + }); + }, + + resetCompose: (composeId = 'compose-modal') => { + set((state) => { + state.composers[composeId] = { + ...state.default, + idempotencyKey: crypto.randomUUID(), + resetFileKey: getResetFileKey(), + ...(composeId.startsWith('reply:') ? { inReplyToId: composeId.slice(6) } : undefined), + ...(composeId.startsWith('group:') + ? { visibility: 'group', groupId: composeId.slice(6) } + : undefined), + }; + }); + }, + + selectComposeSuggestion: (composeId, position, token, suggestion, path) => { + let completion = ''; + let startPosition = position; + + if (typeof suggestion === 'object' && 'id' in suggestion) { + completion = isNativeEmoji(suggestion) ? suggestion.native : suggestion.colons; + startPosition = position - 1; + + useSettingsStore.getState().actions.rememberEmojiUse(suggestion); + lazyStore.dispatch(saveSettings()); + } else if (typeof suggestion === 'string' && suggestion[0] === '#') { + completion = suggestion; + startPosition = position - 1; + } else if (typeof suggestion === 'string') { + completion = selectAccount(suggestion)!.acct; + startPosition = position; + } + + get().actions.updateCompose(composeId, (compose) => { + const updateText = (oldText?: string) => + `${oldText?.slice(0, startPosition)}${completion} ${oldText?.slice(startPosition + (token?.length ?? 0))}`; + if (path[0] === 'spoiler_text') { + compose.spoilerText = updateText(compose.spoilerText); + } else if (compose.poll) { + compose.poll.options[path[2]] = updateText(compose.poll.options[path[2]]); + } + }); + }, + + importDefaultSettings: (account) => { + get().actions.updateCompose('default', (compose) => { + const settings = account.settings_store?.[FE_NAME]; + + if (!settings) return; + + if (settings.defaultPrivacy) compose.visibility = settings.defaultPrivacy; + if (settings.defaultContentType) compose.contentType = settings.defaultContentType; + }); + }, + + importDefaultContentType: (instance) => { + get().actions.updateCompose('default', (compose) => { + const postFormats = instance.pleroma.metadata.post_formats; + + compose.contentType = + postFormats.includes(compose.contentType) || + (postFormats.includes('text/markdown') && compose.contentType === 'wysiwyg') + ? compose.contentType + : postFormats.includes('text/markdown') + ? 'text/markdown' + : postFormats[0]; + }); + }, + + handleTimelineDelete: (statusId) => { + get().actions.updateAllCompose((compose) => { + if (statusId === compose.inReplyToId) { + compose.inReplyToId = null; + } + if (statusId === compose.quoteId) { + compose.quoteId = null; + } + }); + }, + }, + }), + { + enableAutoFreeze: false, + }, + ), +); + +const useSubmitCompose = (composeId: string) => { + const actions = useComposeActions(); + const client = useClient(); + const dispatch = useAppDispatch(); + const features = useFeatures(); + const { openModal, closeModal } = useModalsActions(); + const settings = useSettings(); + + const submitCompose = useCallback( + async (opts: { force?: boolean; preview?: boolean; onSuccess?: () => void } = {}) => { + const { force = false, preview = false, onSuccess } = opts; + + const compose = actions.getCompose(composeId); + + const statusText = compose.text; + const media = compose.mediaAttachments; + const editedId = compose.editedId; + let to = compose.to; + const { forceImplicitAddressing } = settings; + const explicitAddressing = + features.createStatusExplicitAddressing && !forceImplicitAddressing; + + if (!preview) { + const scheduledAt = compose.scheduledAt; + if (scheduledAt) { + const fiveMinutesFromNow = new Date(new Date().getTime() + 300000); + const valid = + scheduledAt.getTime() > fiveMinutesFromNow.getTime() || + (features.scheduledStatusesBackwards && scheduledAt.getTime() < new Date().getTime()); + if (!valid) { + toast.error(messages.scheduleError); + return; + } + } + + if ((!statusText || !statusText.length) && media.length === 0) { + return; + } + + if (!force) { + const missingDescriptionModal = settings.missingDescriptionModal; + const hasMissing = media.some((item) => !item.description); + if (missingDescriptionModal && hasMissing) { + openModal('MISSING_DESCRIPTION', { + onContinue: () => { + closeModal('MISSING_DESCRIPTION'); + submitCompose({ force: true, onSuccess }); + }, + }); + return; + } + } + } + + const mentionsMatch: string[] | null = statusText.match( + /(?:^|\s)@([a-z\d_-]+(?:@(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]+)?)/gi, + ); + + if (mentionsMatch) { + to = [ + ...new Set([ + ...to, + ...mentionsMatch.map((mention) => + mention + .replace(/ /g, '') + .trim() + .slice(1), + ), + ]), + ]; + } + + if (!preview) { + actions.updateCompose(composeId, (draft) => { + draft.isSubmitting = true; + }); + + closeModal('COMPOSE'); + + if (compose.language && !editedId) { + useSettingsStore.getState().actions.rememberLanguageUse(compose.language); + dispatch(saveSettings()); + } + } + + const idempotencyKey = compose.idempotencyKey; + const contentType = compose.contentType === 'wysiwyg' ? 'text/markdown' : compose.contentType; + + const params: CreateStatusParams = { + status: statusText, + in_reply_to_id: compose.inReplyToId ?? undefined, + quote_id: compose.quoteId ?? undefined, + media_ids: media.map((item) => item.id), + sensitive: compose.sensitive, + spoiler_text: compose.spoilerText, + visibility: compose.visibility, + content_type: contentType, + scheduled_at: preview ? undefined : compose.scheduledAt?.toISOString(), + language: compose.language ?? compose.suggestedLanguage ?? undefined, + to: explicitAddressing && to.length ? to : undefined, + local_only: compose.localOnly, + interaction_policy: + (['public', 'unlisted', 'private'].includes(compose.visibility) && + compose.interactionPolicy) || + undefined, + quote_approval_policy: compose.quoteApprovalPolicy ?? undefined, + location_id: compose.location?.origin_id ?? undefined, + }; + + if (compose.editedId) { + // @ts-ignore + params.media_attributes = media.map((item) => { + const focalPoint = (item.type === 'image' || item.type === 'gifv') && item.meta?.focus; + const focus = focalPoint + ? `${focalPoint.x.toFixed(2)},${focalPoint.y.toFixed(2)}` + : undefined; + + return { id: item.id, description: item.description, focus }; + }) as EditStatusParams['media_attributes']; + } + + if (compose.poll) { + params.poll = { + options: compose.poll.options, + expires_in: compose.poll.expires_in, + multiple: compose.poll.multiple, + hide_totals: compose.poll.hide_totals, + options_map: compose.poll.options_map, + }; + } + + if (compose.language && Object.keys(compose.textMap).length) { + params.status_map = compose.textMap; + params.status_map[compose.language] = statusText; + + if (params.spoiler_text) { + params.spoiler_text_map = compose.spoilerTextMap; + params.spoiler_text_map[compose.language] = compose.spoilerText; + } + + const pollParams = params.poll; + if (pollParams?.options_map) { + pollParams.options.forEach( + (option, index: number) => (pollParams.options_map![index][compose.language!] = option), + ); + } + } + + if (compose.visibility === 'group' && compose.groupId) { + params.group_id = compose.groupId; + } + + if (preview) { + try { + const data = await client.statuses.previewStatus(params); + actions.updateCompose(composeId, (draft) => { + draft.preview = data; + }); + onSuccess?.(); + } catch {} + } else { + if (compose.redacting) { + // @ts-ignore + params.overwrite = compose.redactingOverwrite; + } + + try { + const data = await dispatch( + createStatus(params, idempotencyKey, editedId, compose.redacting), + ); + + const draftIdToCancel = compose.draftId; + + actions.resetCompose(composeId); + + if (draftIdToCancel) { + dispatch((_, getState) => { + const accountUrl = selectOwnAccount(getState())!.url; + cancelDraftStatus(queryClient, accountUrl, draftIdToCancel); + }); + } + + if (data.scheduled_at === null) { + const linkOptions: LinkOptions = + data.visibility === 'direct' && features.conversations + ? { to: '/conversations' } + : { + to: '/@{$username}/posts/$statusId', + params: { username: data.account.acct, statusId: data.id }, + }; + toast.success( + compose.redacting + ? messages.redactSuccess + : editedId + ? messages.editSuccess + : messages.success, + { actionLabel: messages.view, actionLinkOptions: linkOptions }, + ); + } else { + toast.success(messages.scheduledSuccess, { + actionLabel: messages.view, + actionLinkOptions: { to: '/scheduled_statuses' }, + }); + } + + onSuccess?.(); + } catch (error) { + actions.updateCompose(composeId, (draft) => { + draft.isSubmitting = false; + }); + } + } + }, + [composeId], + ); + + return submitCompose; +}; + +const useCompose = (composeId: ID extends 'default' ? never : ID): Compose => + useComposeStore((state) => state.composers[composeId] ?? state.default); + +const useComposeActions = () => useComposeStore((state) => state.actions); + +const useUploadCompose = (composeId: string) => { + const { updateCompose } = useComposeActions(); + const instance = useInstance(); + const dispatch = useAppDispatch(); + const intl = useIntl(); + + return useCallback( + (files: FileList) => { + const compose = + useComposeStore.getState().composers[composeId] || useComposeStore.getState().default; + + const attachmentLimit = instance.configuration.statuses.max_media_attachments; + const media = compose.mediaAttachments; + const progress = new Array(files.length).fill(0); + let total = Array.from(files).reduce((a, v) => a + v.size, 0); + const mediaCount = media ? media.length : 0; + + if (files.length + mediaCount > attachmentLimit) { + toast.error(messages.uploadErrorLimit); + return; + } + + updateCompose(composeId, (draft) => { + draft.isUploading = true; + }); + + Array.from(files).forEach((f, i) => { + if (mediaCount + i > attachmentLimit - 1) return; + + dispatch( + uploadFile( + f, + intl, + (data) => + updateCompose(composeId, (draft) => { + appendMedia(draft, data); + }), + () => + updateCompose(composeId, (draft) => { + draft.isUploading = false; + }), + ({ loaded }) => { + progress[i] = loaded; + updateCompose(composeId, (draft) => { + draft.progress = Math.round((progress.reduce((a, v) => a + v, 0) / total) * 100); + }); + }, + (value) => { + total += value; + }, + ), + ); + }); + }, + [instance, composeId], + ); +}; + +const useChangeUploadCompose = (composeId: string) => { + const { updateCompose } = useComposeActions(); + const dispatch = useAppDispatch(); + + return useCallback( + async (mediaId: string, params: UpdateMediaParams) => { + const compose = + useComposeStore.getState().composers[composeId] || useComposeStore.getState().default; + + updateCompose(composeId, (draft) => { + draft.isChangingUpload = true; + }); + + try { + const response = await dispatch(updateMedia(mediaId, params)); + updateCompose(composeId, (draft) => { + draft.isChangingUpload = false; + draft.mediaAttachments = draft.mediaAttachments.map((item) => + item.id === response.id ? response : item, + ); + }); + return response; + } catch (error: any) { + if (error.response?.status === 404 && compose.editedId) { + const previousMedia = compose.mediaAttachments.find((m) => m.id === mediaId); + if (previousMedia) { + updateCompose(composeId, (draft) => { + draft.isChangingUpload = false; + draft.mediaAttachments = draft.mediaAttachments.map((item) => + item.id === mediaId ? { ...previousMedia, ...params } : item, + ); + }); + return; + } + } + updateCompose(composeId, (draft) => { + draft.isChangingUpload = false; + }); + } + }, + [composeId], + ); +}; + +export { + type Compose, + appendMedia, + newPoll, + statusToMentionsAccountIdsArray, + useComposeStore, + useCompose, + useComposeActions, + useSubmitCompose, + useUploadCompose, + useChangeUploadCompose, +}; From e9695ce0186161f0829e04310b0ee21b95c1e745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 22:49:09 +0100 Subject: [PATCH 069/264] nicolium: fix default selectors across the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/hooks/use-compose-suggestions.ts | 3 +-- .../src/modals/select-bookmark-folder-modal.tsx | 2 +- .../src/pages/status-lists/bookmark-folders.tsx | 2 +- .../pl-fe/src/queries/accounts/use-antennas.ts | 13 ++++++++++--- .../pl-fe/src/queries/accounts/use-circles.ts | 13 ++++++++++--- .../src/queries/instance/use-custom-emojis.ts | 10 ++++++---- .../queries/statuses/use-bookmark-folders.ts | 12 +++++++++--- .../src/queries/statuses/use-draft-statuses.ts | 17 ++++++++++++++--- 8 files changed, 52 insertions(+), 20 deletions(-) diff --git a/packages/pl-fe/src/hooks/use-compose-suggestions.ts b/packages/pl-fe/src/hooks/use-compose-suggestions.ts index 5b86ded29..fc761f186 100644 --- a/packages/pl-fe/src/hooks/use-compose-suggestions.ts +++ b/packages/pl-fe/src/hooks/use-compose-suggestions.ts @@ -19,8 +19,7 @@ const useComposeSuggestions = (token: string): Array => { ? 'emojis' : null; - // TODO: fix default selectors across the code - const { data: customEmojis } = useCustomEmojis((emojis) => emojis); + const { data: customEmojis } = useCustomEmojis(); const { data: accountIds } = useAccountSearch(searchedType === 'accounts' ? debouncedToken : '', { resolve: false, limit: 5, diff --git a/packages/pl-fe/src/modals/select-bookmark-folder-modal.tsx b/packages/pl-fe/src/modals/select-bookmark-folder-modal.tsx index 7dbb86488..3b35e5246 100644 --- a/packages/pl-fe/src/modals/select-bookmark-folder-modal.tsx +++ b/packages/pl-fe/src/modals/select-bookmark-folder-modal.tsx @@ -52,7 +52,7 @@ const SelectBookmarkFolderModal: React.FC data); + const { isFetching, data: bookmarkFolders } = useBookmarkFolders(); const { data: selectedBookmarkFolders, isPending: fetchingSelectedBookmarkFolders } = useStatusBookmarkFolders(statusId); const { mutate: addBookmarkToFolder, isPending: addingBookmarkToFolder } = diff --git a/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx b/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx index b1283ad44..41d5e851a 100644 --- a/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx +++ b/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx @@ -98,7 +98,7 @@ const BookmarkFoldersPage: React.FC = () => { const intl = useIntl(); const features = useFeatures(); - const { data: bookmarkFolders, isFetching } = useBookmarkFolders((data) => data); + const { data: bookmarkFolders, isFetching } = useBookmarkFolders(); if (!features.bookmarkFolders) return ; diff --git a/packages/pl-fe/src/queries/accounts/use-antennas.ts b/packages/pl-fe/src/queries/accounts/use-antennas.ts index 068c705b9..9fc130c20 100644 --- a/packages/pl-fe/src/queries/accounts/use-antennas.ts +++ b/packages/pl-fe/src/queries/accounts/use-antennas.ts @@ -1,4 +1,9 @@ -import { type InfiniteData, useMutation, useQuery } from '@tanstack/react-query'; +import { + type InfiniteData, + useMutation, + useQuery, + type UseQueryResult, +} from '@tanstack/react-query'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; @@ -10,7 +15,9 @@ import { minifyAccountList } from '../utils/minify-list'; import type { Antenna, PaginatedResponse, CreateAntennaParams, UpdateAntennaParams } from 'pl-api'; -const useAntennas = (select?: (data: Array) => T) => { +function useAntennas(select: (data: Array) => T): UseQueryResult; +function useAntennas(): UseQueryResult, Error>; +function useAntennas>(select?: (data: Array) => T) { const client = useClient(); const features = useFeatures(); @@ -20,7 +27,7 @@ const useAntennas = (select?: (data: Array) => T) => { enabled: features.antennas, select, }); -}; +} const useAntenna = (antennaId?: string) => useAntennas((data) => (antennaId ? data.find((antenna) => antenna.id === antennaId) : undefined)); diff --git a/packages/pl-fe/src/queries/accounts/use-circles.ts b/packages/pl-fe/src/queries/accounts/use-circles.ts index 9f94d9fdf..88794cb11 100644 --- a/packages/pl-fe/src/queries/accounts/use-circles.ts +++ b/packages/pl-fe/src/queries/accounts/use-circles.ts @@ -1,4 +1,9 @@ -import { type InfiniteData, useMutation, useQuery } from '@tanstack/react-query'; +import { + type InfiniteData, + type UseQueryResult, + useMutation, + useQuery, +} from '@tanstack/react-query'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; @@ -10,7 +15,9 @@ import { minifyAccountList } from '../utils/minify-list'; import type { Circle, PaginatedResponse } from 'pl-api'; -const useCircles = (select?: (data: Array) => T) => { +function useCircles(select: (data: Array) => T): UseQueryResult; +function useCircles(): UseQueryResult, Error>; +function useCircles>(select?: (data: Array) => T) { const client = useClient(); const features = useFeatures(); @@ -20,7 +27,7 @@ const useCircles = (select?: (data: Array) => T) => { enabled: features.circles, select, }); -}; +} const useCircle = (circleId?: string) => useCircles((data) => (circleId ? data.find((circle) => circle.id === circleId) : undefined)); diff --git a/packages/pl-fe/src/queries/instance/use-custom-emojis.ts b/packages/pl-fe/src/queries/instance/use-custom-emojis.ts index 5eb993c9c..4eba4a5b2 100644 --- a/packages/pl-fe/src/queries/instance/use-custom-emojis.ts +++ b/packages/pl-fe/src/queries/instance/use-custom-emojis.ts @@ -1,4 +1,4 @@ -import { queryOptions, useQuery } from '@tanstack/react-query'; +import { queryOptions, useQuery, type UseQueryResult } from '@tanstack/react-query'; import { buildCustomEmojis } from '@/features/emoji'; import { addCustomToPool } from '@/features/emoji/search'; @@ -18,14 +18,16 @@ const customEmojisQueryOptions = (client: PlApiClient) => }), }); -const useCustomEmojis = (select?: (data: Array) => T) => { +function useCustomEmojis(select: (data: Array) => T): UseQueryResult; +function useCustomEmojis(): UseQueryResult, Error>; +function useCustomEmojis>(select?: (data: Array) => T) { const client = useClient(); return useQuery({ ...customEmojisQueryOptions(client), - select: select ?? ((data) => data as T), + select, }); -}; +} const prefetchCustomEmojis = (client: PlApiClient) => queryClient.prefetchQuery(customEmojisQueryOptions(client)); diff --git a/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts b/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts index 22ea2667a..4e7d7bb5c 100644 --- a/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts +++ b/packages/pl-fe/src/queries/statuses/use-bookmark-folders.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { useMutation, useQuery, type UseQueryResult } from '@tanstack/react-query'; import { useClient } from '@/hooks/use-client'; import { useFeatures } from '@/hooks/use-features'; @@ -7,7 +7,13 @@ import { queryClient } from '../client'; import type { BookmarkFolder } from 'pl-api'; -const useBookmarkFolders = (select?: (data: Array) => T) => { +function useBookmarkFolders( + select: (data: Array) => T, +): UseQueryResult; +function useBookmarkFolders(): UseQueryResult, Error>; +function useBookmarkFolders>( + select?: (data: Array) => T, +) { const client = useClient(); const features = useFeatures(); @@ -17,7 +23,7 @@ const useBookmarkFolders = (select?: (data: Array) => T) => { enabled: features.bookmarkFolders, select, }); -}; +} const useBookmarkFolder = (folderId?: string) => useBookmarkFolders((data) => diff --git a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts index ea2a3750c..5d5dbc82a 100644 --- a/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts +++ b/packages/pl-fe/src/queries/statuses/use-draft-statuses.ts @@ -1,4 +1,9 @@ -import { type QueryClient, useQuery, useQueryClient } from '@tanstack/react-query'; +import { + type QueryClient, + useQuery, + useQueryClient, + type UseQueryResult, +} from '@tanstack/react-query'; import { create } from 'mutative'; import { mediaAttachmentSchema } from 'pl-api'; import * as v from 'valibot'; @@ -53,7 +58,13 @@ const getDrafts = async (accountUrl: string) => { const persistDrafts = (accountUrl: string, drafts: Record) => KVStore.setItem(`drafts:${accountUrl}`, Object.values(drafts)); -const useDraftStatusesQuery = (select?: (data: Record) => T) => { +function useDraftStatusesQuery( + select: (data: Record) => T, +): UseQueryResult; +function useDraftStatusesQuery(): UseQueryResult, Error>; +function useDraftStatusesQuery>( + select?: (data: Record) => T, +) { const { data: account } = useOwnAccount(); return useQuery({ @@ -62,7 +73,7 @@ const useDraftStatusesQuery = (select?: (data: Record) = enabled: !!account, select, }); -}; +} const useDraftStatusQuery = (draftStatusId: string) => useDraftStatusesQuery((data) => data[draftStatusId]); From 01d96998e4a9f39fdcbf2417eb056cd2a71da260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 23:28:59 +0100 Subject: [PATCH 070/264] nicolium: random a11y improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/account.tsx | 16 ++++++++-- .../components/authorize-reject-buttons.tsx | 13 ++++++++- .../pl-fe/src/components/preview-card.tsx | 13 ++++++++- .../auth-login/components/consumer-button.tsx | 1 + .../chats/components/chat-composer.tsx | 2 ++ .../components/chat-search/chat-search.tsx | 9 +++++- .../features/chats/components/chat-upload.tsx | 28 ++++++++++++------ .../chats-page/components/chats-page-chat.tsx | 2 ++ .../chats-page/components/chats-page-new.tsx | 2 ++ .../components/chats-page-settings.tsx | 2 ++ .../components/chats-page-shoutbox.tsx | 8 ++++- .../components/chats-page-sidebar.tsx | 4 +++ .../chats/components/shoutbox-composer.tsx | 1 + .../compose-event/tabs/edit-event.tsx | 12 +++++++- .../compose/editor/nodes/image-component.tsx | 4 +++ .../plugins/floating-link-editor-plugin.tsx | 29 ++++++++++++++++++- .../floating-text-format-toolbar-plugin.tsx | 4 ++- .../edit-profile/components/header-picker.tsx | 8 ++++- .../group/components/group-options-button.tsx | 2 ++ packages/pl-fe/src/locales/en.json | 18 ++++++++++++ .../pl-fe/src/pages/dashboard/reports.tsx | 2 ++ packages/pl-fe/src/pages/search/search.tsx | 2 ++ .../pages/settings/rss-feed-subscriptions.tsx | 2 ++ .../src/pages/timelines/remote-timeline.tsx | 8 ++++- 24 files changed, 172 insertions(+), 20 deletions(-) diff --git a/packages/pl-fe/src/components/account.tsx b/packages/pl-fe/src/components/account.tsx index 3de4f3f09..1fe176f2d 100644 --- a/packages/pl-fe/src/components/account.tsx +++ b/packages/pl-fe/src/components/account.tsx @@ -65,7 +65,14 @@ const InstanceFavicon: React.FC = ({ account, disabled }) => { const className = 'size-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'; if (disabled) { - return ; + return ( + {account.domain} + ); } return ( @@ -75,7 +82,12 @@ const InstanceFavicon: React.FC = ({ account, disabled }) => { disabled={disabled} title={intl.formatMessage(messages.timeline, { domain: account.domain })} > - + {account.domain} ); }; diff --git a/packages/pl-fe/src/components/authorize-reject-buttons.tsx b/packages/pl-fe/src/components/authorize-reject-buttons.tsx index 524f146f8..a65770f54 100644 --- a/packages/pl-fe/src/components/authorize-reject-buttons.tsx +++ b/packages/pl-fe/src/components/authorize-reject-buttons.tsx @@ -1,11 +1,16 @@ import clsx from 'clsx'; import React, { useEffect, useRef, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import HStack from '@/components/ui/hstack'; import IconButton from '@/components/ui/icon-button'; import Text from '@/components/ui/text'; +const messages = defineMessages({ + authorize: { id: 'authorize.action', defaultMessage: 'Approve' }, + reject: { id: 'reject.action', defaultMessage: 'Reject' }, +}); + interface IAuthorizeRejectButtons { onAuthorize(): Promise | unknown; onReject(): Promise | unknown; @@ -18,6 +23,7 @@ const AuthorizeRejectButtons: React.FC = ({ onReject, countdown, }) => { + const intl = useIntl(); const [state, setState] = useState< 'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending' >('pending'); @@ -129,6 +135,7 @@ const AuthorizeRejectButtons: React.FC = ({ isLoading={state === 'rejecting'} disabled={state === 'authorizing'} style={renderStyle('rejecting')} + title={intl.formatMessage(messages.reject)} /> = ({ isLoading={state === 'authorizing'} disabled={state === 'rejecting'} style={renderStyle('authorizing')} + title={intl.formatMessage(messages.authorize)} /> ); @@ -162,6 +170,7 @@ interface IAuthorizeRejectButton { isLoading?: boolean; disabled?: boolean; style: React.CSSProperties; + title?: string; } const AuthorizeRejectButton: React.FC = ({ @@ -171,6 +180,7 @@ const AuthorizeRejectButton: React.FC = ({ isLoading, style, disabled, + title, }) => (
= ({ 'text-danger-600': theme === 'danger', })} disabled={disabled} + title={title} />
diff --git a/packages/pl-fe/src/components/preview-card.tsx b/packages/pl-fe/src/components/preview-card.tsx index dee55b1a4..c3af67034 100644 --- a/packages/pl-fe/src/components/preview-card.tsx +++ b/packages/pl-fe/src/components/preview-card.tsx @@ -7,7 +7,7 @@ import { mediaAttachmentSchema, } from 'pl-api'; import React, { useState, useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import * as v from 'valibot'; import Blurhash from '@/components/blurhash'; @@ -23,6 +23,12 @@ import Purify from '@/utils/url-purify'; import HoverAccountWrapper from './hover-account-wrapper'; import Avatar from './ui/avatar'; +const messages = defineMessages({ + play: { id: 'preview_card.play', defaultMessage: 'Play' }, + expand: { id: 'preview_card.expand', defaultMessage: 'Enlarge image' }, + externalLink: { id: 'preview_card.external_link', defaultMessage: 'Open in new tab' }, +}); + const domParser = new DOMParser(); const handleIframeUrl = (html: string, url: string, providerName: string) => { @@ -106,6 +112,7 @@ const PreviewCard: React.FC = ({ cacheWidth, onOpenMedia, }): React.JSX.Element => { + const intl = useIntl(); const { urlPrivacy: { clearLinksInContent, redirectLinksMode }, } = useSettings(); @@ -253,6 +260,9 @@ const PreviewCard: React.FC = ({ @@ -266,6 +276,7 @@ const PreviewCard: React.FC = ({ target='_blank' rel='noopener' className='text-gray-700 hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100' + title={intl.formatMessage(messages.externalLink)} > = ({ provider }) => { iconClassName='h-6 w-6' src={icon} onClick={handleClick} + title={intl.formatMessage(messages.tooltip, { provider: capitalize(provider) })} /> ); diff --git a/packages/pl-fe/src/features/chats/components/chat-composer.tsx b/packages/pl-fe/src/features/chats/components/chat-composer.tsx index 18957dd8a..3e0bede28 100644 --- a/packages/pl-fe/src/features/chats/components/chat-composer.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-composer.tsx @@ -30,6 +30,7 @@ import type { MediaAttachment } from 'pl-api'; const messages = defineMessages({ placeholder: { id: 'chat.input.placeholder', defaultMessage: 'Type a message' }, + send: { id: 'chat.actions.send', defaultMessage: 'Send' }, unblockMessage: { id: 'chat_settings.unblock.message', defaultMessage: @@ -256,6 +257,7 @@ const ChatComposer = React.forwardRef className='text-primary-500' disabled={isSubmitDisabled} onClick={onSubmit} + title={intl.formatMessage(messages.send)} /> diff --git a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx index b359708d9..9ff865a3c 100644 --- a/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-search/chat-search.tsx @@ -21,6 +21,8 @@ import type { PlfeResponse } from '@/api'; const messages = defineMessages({ placeholder: { id: 'chat_search.placeholder', defaultMessage: 'Type a name' }, + clearSearch: { id: 'chat_search.clear', defaultMessage: 'Clear search' }, + search: { id: 'chat_search.search', defaultMessage: 'Search' }, }); interface IChatSearch { @@ -101,7 +103,12 @@ const ChatSearch: React.FC = ({ isMainPage = false }) => { outerClassName='mt-0' theme='search' append={ - -); +const RemoveButton: React.FC = ({ onClick }) => { + const intl = useIntl(); + + return ( + + ); +}; export { ChatUpload as default }; diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-chat.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-chat.tsx index 1ff037c4b..7de81cfda 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-chat.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-chat.tsx @@ -46,6 +46,7 @@ const messages = defineMessages({ blockUser: { id: 'chat_settings.options.block_user', defaultMessage: 'Block @{acct}' }, unblockUser: { id: 'chat_settings.options.unblock_user', defaultMessage: 'Unblock @{acct}' }, leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave chat' }, + back: { id: 'chats.back', defaultMessage: 'Back to chats' }, }); const ChatsPageChat = () => { @@ -129,6 +130,7 @@ const ChatsPageChat = () => { src={require('@phosphor-icons/core/regular/arrow-left.svg')} className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180' onClick={() => navigate({ to: '/chats' })} + title={intl.formatMessage(messages.back)} /> diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-new.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-new.tsx index a868d37a4..06fd4eeae 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-new.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-new.tsx @@ -11,6 +11,7 @@ import ChatSearch from '../../chat-search/chat-search'; const messages = defineMessages({ title: { id: 'chat.new_message.title', defaultMessage: 'New Message' }, + back: { id: 'chats.back', defaultMessage: 'Back to chats' }, }); /** New message form to create a chat. */ @@ -26,6 +27,7 @@ const ChatsPageNew: React.FC = () => { src={require('@phosphor-icons/core/regular/arrow-left.svg')} className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180' onClick={() => navigate({ to: '/chats' })} + title={intl.formatMessage(messages.back)} /> diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx index ad0fd4329..573100299 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-settings.tsx @@ -40,6 +40,7 @@ const messages = defineMessages({ defaultMessage: 'Chat settings updated successfully', }, fail: { id: 'settings.messages.fail', defaultMessage: 'Failed to update chat settings' }, + back: { id: 'chats.back', defaultMessage: 'Back to chats' }, }); const ChatsPageSettings = () => { @@ -78,6 +79,7 @@ const ChatsPageSettings = () => { src={require('@phosphor-icons/core/regular/arrow-left.svg')} className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180' onClick={() => navigate({ to: '/chats' })} + title={intl.formatMessage(messages.back)} /> diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-shoutbox.tsx index 6052ef6d6..fd5dbd3c6 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-shoutbox.tsx @@ -1,6 +1,6 @@ import { useNavigate } from '@tanstack/react-router'; import React from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Avatar from '@/components/ui/avatar'; import HStack from '@/components/ui/hstack'; @@ -12,8 +12,13 @@ import { useInstance } from '@/hooks/use-instance'; import Shoutbox from '../../shoutbox'; +const messages = defineMessages({ + back: { id: 'chats.back', defaultMessage: 'Back to chats' }, +}); + const ChatsPageShoutbox = () => { const navigate = useNavigate(); + const intl = useIntl(); const instance = useInstance(); const { logo } = useFrontendConfig(); @@ -26,6 +31,7 @@ const ChatsPageShoutbox = () => { src={require('@phosphor-icons/core/regular/arrow-left.svg')} className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180' onClick={() => navigate({ to: '/chats' })} + title={intl.formatMessage(messages.back)} /> diff --git a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-sidebar.tsx b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-sidebar.tsx index 26adba16b..66f7f3fcf 100644 --- a/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-sidebar.tsx +++ b/packages/pl-fe/src/features/chats/components/chats-page/components/chats-page-sidebar.tsx @@ -13,6 +13,8 @@ import type { Chat } from 'pl-api'; const messages = defineMessages({ title: { id: 'column.chats', defaultMessage: 'Chats' }, + settings: { id: 'chat_list_item.settings', defaultMessage: 'Chat settings' }, + newChat: { id: 'chat_pane.header.new_chat', defaultMessage: 'New chat' }, }); const ChatsPageSidebar = () => { @@ -49,12 +51,14 @@ const ChatsPageSidebar = () => { src={require('@phosphor-icons/core/regular/sliders-horizontal.svg')} iconClassName='h-5 w-5 text-gray-600' onClick={handleSettingsClick} + title={intl.formatMessage(messages.settings)} /> diff --git a/packages/pl-fe/src/features/chats/components/shoutbox-composer.tsx b/packages/pl-fe/src/features/chats/components/shoutbox-composer.tsx index 1fc8bc300..3670f803a 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox-composer.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox-composer.tsx @@ -108,6 +108,7 @@ const ShoutboxComposer = React.forwardRef diff --git a/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx b/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx index b1e5b3460..a2279b7d7 100644 --- a/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx +++ b/packages/pl-fe/src/features/compose-event/tabs/edit-event.tsx @@ -57,6 +57,10 @@ const messages = defineMessages({ id: 'compose_event.header_description', defaultMessage: 'Add header alt text.', }, + eventHeaderDescriptionPlaceholder: { + id: 'compose_event.header_description_placeholder', + defaultMessage: 'Event banner', + }, }); interface IEditEvent { @@ -263,7 +267,13 @@ const EditEvent: React.FC = ({ statusId }) => {
{banner ? ( <> - + { diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx index cf481b3af..a1f9b054e 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-link-editor-plugin.tsx @@ -19,6 +19,7 @@ import { import { useCallback, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { createPortal } from 'react-dom'; +import { defineMessages, useIntl } from 'react-intl'; import Icon from '@/components/ui/icon'; @@ -26,6 +27,11 @@ import { getSelectedNode } from '../utils/get-selected-node'; import { setFloatingElemPosition } from '../utils/set-floating-elem-position'; import { sanitizeUrl } from '../utils/url'; +const messages = defineMessages({ + editLink: { id: 'compose_form.lexical.edit_link', defaultMessage: 'Edit link' }, + removeLink: { id: 'compose_form.lexical.remove_link', defaultMessage: 'Remove link' }, +}); + const FloatingLinkEditor = ({ editor, anchorElem, @@ -39,6 +45,8 @@ const FloatingLinkEditor = ({ const [isEditMode, setEditMode] = useState(false); const [lastSelection, setLastSelection] = useState(null); + const intl = useIntl(); + const updateLinkEditor = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { @@ -185,14 +193,25 @@ const FloatingLinkEditor = ({ className='absolute inset-y-0 right-0 flex w-9 cursor-pointer items-center justify-center' role='button' tabIndex={0} + aria-label={intl.formatMessage(messages.removeLink)} onMouseDown={(event) => { event.preventDefault(); }} onClick={() => { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); }} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); + } + }} > - +
) : ( @@ -209,16 +228,24 @@ const FloatingLinkEditor = ({ className='absolute inset-y-0 right-0 flex w-9 cursor-pointer items-center justify-center' role='button' tabIndex={0} + aria-label={intl.formatMessage(messages.editLink)} onMouseDown={(event) => { event.preventDefault(); }} onClick={() => { setEditMode(true); }} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setEditMode(true); + } + }} >
diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx index 55297e361..6563e588f 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx @@ -62,6 +62,7 @@ const messages = defineMessages({ defaultMessage: 'Insert code block', }, insertLink: { id: 'compose_form.lexical.insert_link', defaultMessage: 'Insert link' }, + blockType: { id: 'compose_form.lexical.block_type', defaultMessage: 'Change block type' }, }); const blockTypeToIcon = { @@ -124,6 +125,7 @@ const BlockTypeDropdown = ({ icon: string; }) => { const { composeAllowHeadings } = useFeatures(); + const intl = useIntl(); const [showDropDown, setShowDropDown] = useState(false); @@ -201,7 +203,7 @@ const BlockTypeDropdown = ({ setShowDropDown(!showDropDown); }} className='relative flex cursor-pointer rounded-lg border-0 bg-none p-1 align-middle hover:bg-gray-100 disabled:cursor-not-allowed disabled:hover:bg-none dark:hover:bg-primary-700' - aria-label='' + aria-label={intl.formatMessage(messages.blockType)} type='button' > diff --git a/packages/pl-fe/src/features/edit-profile/components/header-picker.tsx b/packages/pl-fe/src/features/edit-profile/components/header-picker.tsx index ab4ac15ef..a08fabf7e 100644 --- a/packages/pl-fe/src/features/edit-profile/components/header-picker.tsx +++ b/packages/pl-fe/src/features/edit-profile/components/header-picker.tsx @@ -82,7 +82,13 @@ const HeaderPicker = React.forwardRef( title={intl.formatMessage(messages.title)} tabIndex={0} > - {src && } + {src && ( + {intl.formatMessage(messages.title)} + )} { iconClassName='h-5 w-5' className='self-stretch px-2.5' data-testid='dropdown-menu-button' + title={intl.formatMessage(messages.groupOptions)} /> ); diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 17fbbd0e8..95965bf87 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -326,6 +326,7 @@ "auth.awaiting_approval": "Your account is awaiting approval", "auth.invalid_credentials": "Wrong username or password", "auth.logged_out": "Logged out.", + "authorize.action": "Approve", "authorize.success": "Approved", "backups.actions.create": "Create backup", "backups.download": "Download", @@ -357,6 +358,7 @@ "bundle_column_error.retry": "Try again", "bundle_column_error.title": "Network error", "card.back.label": "Back", + "chat.actions.remove_attachment": "Remove attachment", "chat.actions.send": "Send", "chat.failed_to_send": "Message failed to send.", "chat.input.placeholder": "Type a message", @@ -387,9 +389,11 @@ "chat_pane.header.new_chat": "New chat", "chat_search.blankslate.body": "Search for someone to chat with.", "chat_search.blankslate.title": "Start a chat", + "chat_search.clear": "Clear search", "chat_search.empty_results_blankslate.body": "Try searching for another name.", "chat_search.empty_results_blankslate.title": "No matches found", "chat_search.placeholder": "Type a name", + "chat_search.search": "Search", "chat_search.title": "Messages", "chat_settings.block.confirm": "Block", "chat_settings.block.heading": "Block @{acct}", @@ -408,6 +412,7 @@ "chats.actions.delete": "Delete for both", "chats.actions.delete_for_me": "Delete for me", "chats.actions.more": "More", + "chats.back": "Back to chats", "chats.dividers.today": "Today", "chats.main.blankslate.new_chat": "Message someone", "chats.main.blankslate.subtitle": "Search for someone to chat with", @@ -441,6 +446,7 @@ "column.admin.moderation_log": "Moderation log", "column.admin.relays": "Instance relays", "column.admin.reports": "Reports", + "column.admin.reports.clear_filter": "Clear filter", "column.admin.reports.filter_message": "You are displaying reports {query}.", "column.admin.reports.filter_message.account": "from @{acct}", "column.admin.reports.filter_message.target_account": "targeting @{acct}", @@ -606,6 +612,7 @@ "compose_event.fields.start_time_label": "Event start date", "compose_event.fields.start_time_placeholder": "Event begins on…", "compose_event.header_description": "Add header alt text.", + "compose_event.header_description_placeholder": "Event banner", "compose_event.participation_requests.authorize": "Authorize", "compose_event.participation_requests.authorize.fail": "Failed to authorize event participation request", "compose_event.participation_requests.authorize.success": "Event participation request authorized successfully", @@ -628,13 +635,16 @@ "compose_form.event_placeholder": "Post to this event", "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.interaction_policy.label": "Manage interaction policy", + "compose_form.lexical.block_type": "Change block type", "compose_form.lexical.create_horizontal_line": "Create horizontal line", + "compose_form.lexical.edit_link": "Edit link", "compose_form.lexical.format_bold": "Format bold", "compose_form.lexical.format_italic": "Format italic", "compose_form.lexical.format_strikethrough": "Format strikethrough", "compose_form.lexical.format_underline": "Format underline", "compose_form.lexical.insert_code_block": "Insert code block", "compose_form.lexical.insert_link": "Insert link", + "compose_form.lexical.remove_link": "Remove link", "compose_form.lexical.upload_media": "Upload media", "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", "compose_form.lock_disclaimer.lock": "locked", @@ -1095,6 +1105,7 @@ "group.manage": "Manage group", "group.member.admin.limit.summary": "You can assign up to {count, plural, one {admin} other {admins}} for the group at this time.", "group.member.admin.limit.title": "Admin limit reached", + "group.options": "Group options", "group.popover.action": "View group", "group.popover.summary": "You must be a member of the group in order to reply to this status.", "group.popover.title": "Membership required", @@ -1583,6 +1594,9 @@ "preferences.options.privacy_followers_only": "Followers-only", "preferences.options.privacy_public": "Public", "preferences.options.privacy_unlisted": "Unlisted", + "preview_card.expand": "Enlarge image", + "preview_card.external_link": "Open in new tab", + "preview_card.play": "Play", "privacy.change": "Adjust post privacy", "privacy.circle.long": "Visible to members of a circle", "privacy.circle.short": "Circle only", @@ -1632,6 +1646,7 @@ "registration.sign_up": "Sign up", "registration.tos": "Terms of Service", "registration.username_unavailable": "Username is already taken.", + "reject.action": "Reject", "reject.success": "Rejected", "relative_time.days": "{number}d", "relative_time.hours": "{number}h", @@ -1662,6 +1677,7 @@ "remote_interaction.reply": "Proceed to reply", "remote_interaction.reply_title": "Reply to a post remotely", "remote_interaction.user_not_found_error": "Couldn't find given user", + "remote_timeline.close": "Close remote timeline", "remote_timeline.filter_message": "You are viewing the timeline of {instance}.", "reply_indicator.cancel": "Cancel", "reply_mentions.account.add": "Add to mentions", @@ -1702,6 +1718,7 @@ "rss_feed.label": "RSS Feed", "rss_feed_subscriptions.add.fail": "Failed to subsrcibe to RSS feed", "rss_feed_subscriptions.add.success": "Successfully subscribed to RSS feed", + "rss_feed_subscriptions.delete": "Delete feed", "rss_feed_subscriptions.list.heading": "Subscribed feeds", "rss_feed_subscriptions.new.create_title": "Subscribe", "rss_feed_subscriptions.new.heading": "Subscribe to a new RSS feed", @@ -1714,6 +1731,7 @@ "scheduled_status.cancel": "Cancel", "search.action": "Search for “{query}”", "search.clear": "Clear input", + "search.clear_account_filter": "Clear account filter", "search.placeholder": "Search", "search_results.accounts": "People", "search_results.filter_message": "You are searching for posts from @{acct}.", diff --git a/packages/pl-fe/src/pages/dashboard/reports.tsx b/packages/pl-fe/src/pages/dashboard/reports.tsx index a7353d87f..ce53d37e8 100644 --- a/packages/pl-fe/src/pages/dashboard/reports.tsx +++ b/packages/pl-fe/src/pages/dashboard/reports.tsx @@ -14,6 +14,7 @@ import { useReports } from '@/queries/admin/use-reports'; const messages = defineMessages({ heading: { id: 'column.admin.reports', defaultMessage: 'Reports' }, + clearFilter: { id: 'column.admin.reports.clear_filter', defaultMessage: 'Clear filter' }, }); const Reports: React.FC = () => { @@ -54,6 +55,7 @@ const Reports: React.FC = () => { iconClassName='h-5 w-5' src={require('@phosphor-icons/core/regular/x.svg')} onClick={handleUnsetAccounts} + title={intl.formatMessage(messages.clearFilter)} /> { iconClassName='h-5 w-5' src={require('@phosphor-icons/core/regular/x.svg')} onClick={handleUnsetAccount} + title={intl.formatMessage(messages.clearAccountFilter)} /> { disabled={isPending} className='size-8 text-gray-700 dark:text-gray-600' src={require('@phosphor-icons/core/regular/x.svg')} + title={intl.formatMessage(messages.deleteFeed)} /> } diff --git a/packages/pl-fe/src/pages/timelines/remote-timeline.tsx b/packages/pl-fe/src/pages/timelines/remote-timeline.tsx index e1367c1c1..c991ebc9c 100644 --- a/packages/pl-fe/src/pages/timelines/remote-timeline.tsx +++ b/packages/pl-fe/src/pages/timelines/remote-timeline.tsx @@ -1,6 +1,6 @@ import { useNavigate } from '@tanstack/react-router'; import React, { useEffect } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchPublicTimeline } from '@/actions/timelines'; import { useRemoteStream } from '@/api/hooks/streaming/use-remote-stream'; @@ -14,10 +14,15 @@ import { remoteTimelineRoute } from '@/features/ui/router'; import { useAppDispatch } from '@/hooks/use-app-dispatch'; import { useSettings } from '@/stores/settings'; +const messages = defineMessages({ + close: { id: 'remote_timeline.close', defaultMessage: 'Close remote timeline' }, +}); + /** View statuses from a remote instance. */ const RemoteTimelinePage: React.FC = () => { const { instance } = remoteTimelineRoute.useParams(); + const intl = useIntl(); const navigate = useNavigate(); const dispatch = useAppDispatch(); @@ -53,6 +58,7 @@ const RemoteTimelinePage: React.FC = () => { iconClassName='h-5 w-5' src={require('@phosphor-icons/core/regular/x.svg')} onClick={handleCloseClick} + title={intl.formatMessage(messages.close)} /> Date: Wed, 25 Feb 2026 23:41:41 +0100 Subject: [PATCH 071/264] nicolium: label remaining text editor buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../floating-text-format-toolbar-plugin.tsx | 22 +++++++++++++++++++ packages/pl-fe/src/locales/en.json | 8 +++++++ 2 files changed, 30 insertions(+) diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx index 6563e588f..61de50137 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx @@ -63,6 +63,14 @@ const messages = defineMessages({ }, insertLink: { id: 'compose_form.lexical.insert_link', defaultMessage: 'Insert link' }, blockType: { id: 'compose_form.lexical.block_type', defaultMessage: 'Change block type' }, + paragraph: { id: 'compose_form.lexical.block_type.paragraph', defaultMessage: 'Normal' }, + h1: { id: 'compose_form.lexical.block_type.h1', defaultMessage: 'Heading 1' }, + h2: { id: 'compose_form.lexical.block_type.h2', defaultMessage: 'Heading 2' }, + h3: { id: 'compose_form.lexical.block_type.h3', defaultMessage: 'Heading 3' }, + bulletList: { id: 'compose_form.lexical.block_type.bullet', defaultMessage: 'Bulleted list' }, + numberedList: { id: 'compose_form.lexical.block_type.number', defaultMessage: 'Numbered list' }, + quote: { id: 'compose_form.lexical.block_type.quote', defaultMessage: 'Quote' }, + codeBlock: { id: 'compose_form.lexical.block_type.code', defaultMessage: 'Code block' }, }); const blockTypeToIcon = { @@ -217,6 +225,7 @@ const BlockTypeDropdown = ({ onClick={formatParagraph} active={blockType === 'paragraph'} icon={blockTypeToIcon.paragraph} + aria-label={intl.formatMessage(messages.paragraph)} /> {composeAllowHeadings && ( <> @@ -226,6 +235,7 @@ const BlockTypeDropdown = ({ }} active={blockType === 'h1'} icon={blockTypeToIcon.h1} + aria-label={intl.formatMessage(messages.h1)} /> { @@ -233,6 +243,7 @@ const BlockTypeDropdown = ({ }} active={blockType === 'h2'} icon={blockTypeToIcon.h2} + aria-label={intl.formatMessage(messages.h2)} /> { @@ -240,6 +251,7 @@ const BlockTypeDropdown = ({ }} active={blockType === 'h3'} icon={blockTypeToIcon.h3} + aria-label={intl.formatMessage(messages.h3)} /> )} @@ -247,21 +259,25 @@ const BlockTypeDropdown = ({ onClick={formatBulletList} active={blockType === 'bullet'} icon={blockTypeToIcon.bullet} + aria-label={intl.formatMessage(messages.bulletList)} />
)} @@ -390,6 +406,7 @@ const TextFormatFloatingToolbar = ({ }} active={isBold} aria-label={intl.formatMessage(messages.formatBold)} + aria-label={intl.formatMessage(messages.formatBold)} icon={require('@phosphor-icons/core/regular/text-b.svg')} /> diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index 95965bf87..b40391cdf 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -636,6 +636,14 @@ "compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag.", "compose_form.interaction_policy.label": "Manage interaction policy", "compose_form.lexical.block_type": "Change block type", + "compose_form.lexical.block_type.bullet": "Bulleted list", + "compose_form.lexical.block_type.code": "Code block", + "compose_form.lexical.block_type.h1": "Heading 1", + "compose_form.lexical.block_type.h2": "Heading 2", + "compose_form.lexical.block_type.h3": "Heading 3", + "compose_form.lexical.block_type.number": "Numbered list", + "compose_form.lexical.block_type.paragraph": "Normal", + "compose_form.lexical.block_type.quote": "Quote", "compose_form.lexical.create_horizontal_line": "Create horizontal line", "compose_form.lexical.edit_link": "Edit link", "compose_form.lexical.format_bold": "Format bold", From 1e029b5d057ea5e62563ce016d986b4bd7dbaf8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 23:42:42 +0100 Subject: [PATCH 072/264] nicolium: what is IComponentInterface supposed to mean lol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/components/ui/tabs.tsx | 4 ++-- .../pl-fe/src/features/chats/components/chat-list-item.tsx | 4 ++-- .../src/features/chats/components/chat-list-shoutbox.tsx | 4 ++-- packages/pl-fe/src/features/chats/components/chat.tsx | 4 ++-- packages/pl-fe/src/features/chats/components/shoutbox.tsx | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/pl-fe/src/components/ui/tabs.tsx b/packages/pl-fe/src/components/ui/tabs.tsx index 9ee642e26..075df08b4 100644 --- a/packages/pl-fe/src/components/ui/tabs.tsx +++ b/packages/pl-fe/src/components/ui/tabs.tsx @@ -18,7 +18,7 @@ import './tabs.css'; const HORIZONTAL_PADDING = 8; const AnimatedContext = React.createContext(null); -interface IAnimatedInterface { +interface IAnimatedTabs { /** Callback when a tab is chosen. */ onChange(index: number): void; /** Default tab index. */ @@ -27,7 +27,7 @@ interface IAnimatedInterface { } /** Tabs with a sliding active state. */ -const AnimatedTabs: React.FC = ({ children, ...rest }) => { +const AnimatedTabs: React.FC = ({ children, ...rest }) => { const [activeRect, setActiveRect] = React.useState(null); const ref = React.useRef(null); const rect = useRect(ref); diff --git a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx index 75b1a43c5..83e84ad13 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx @@ -31,12 +31,12 @@ const messages = defineMessages({ settings: { id: 'chat_list_item.settings', defaultMessage: 'Chat settings' }, }); -interface IChatListItemInterface { +interface IChatListItem { chat: Chat; onClick: (chat: Chat) => void; } -const ChatListItem: React.FC = React.memo(({ chat, onClick }) => { +const ChatListItem: React.FC = React.memo(({ chat, onClick }) => { const { openModal } = useModalsActions(); const intl = useIntl(); const features = useFeatures(); diff --git a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx index f3773e075..32e6a063d 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx @@ -11,11 +11,11 @@ import { useShoutboxMessages } from '@/stores/shoutbox'; import type { Chat } from 'pl-api'; -interface IChatListShoutboxInterface { +interface IChatListShoutbox { onClick: (chat: Chat | 'shoutbox') => void; } -const ChatListShoutbox: React.FC = ({ onClick }) => { +const ChatListShoutbox: React.FC = ({ onClick }) => { const instance = useInstance(); const { logo } = useFrontendConfig(); const messages = useShoutboxMessages(); diff --git a/packages/pl-fe/src/features/chats/components/chat.tsx b/packages/pl-fe/src/features/chats/components/chat.tsx index 02fc515f7..cb0c5208a 100644 --- a/packages/pl-fe/src/features/chats/components/chat.tsx +++ b/packages/pl-fe/src/features/chats/components/chat.tsx @@ -21,7 +21,7 @@ const messages = defineMessages({ uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, }); -interface ChatInterface { +interface IChat { chat: ChatEntity; inputRef?: MutableRefObject; className?: string; @@ -50,7 +50,7 @@ const clearNativeInputValue = (element: HTMLTextAreaElement) => { * Chat UI with just the messages and textarea. * Reused between floating desktop chats and fullscreen/mobile chats. */ -const Chat: React.FC = ({ chat, inputRef, className }) => { +const Chat: React.FC = ({ chat, inputRef, className }) => { const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/packages/pl-fe/src/features/chats/components/shoutbox.tsx b/packages/pl-fe/src/features/chats/components/shoutbox.tsx index abf618679..1ea3d97a1 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox.tsx @@ -10,12 +10,12 @@ import ShoutboxMessageList from './shoutbox-message-list'; const fileKeyGen = (): number => Math.floor(Math.random() * 0x10000); -interface ChatInterface { +interface IShoutbox { inputRef?: MutableRefObject; className?: string; } -const Shoutbox: React.FC = ({ inputRef, className }) => { +const Shoutbox: React.FC = ({ inputRef, className }) => { const [content, setContent] = useState(''); const [resetContentKey, setResetContentKey] = useState(fileKeyGen()); const [errorMessage] = useState(); From d7852da5e526d89ad03ad1510472d7c87d682954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 23:44:24 +0100 Subject: [PATCH 073/264] nicolium: cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../src/components/groups/group-avatar.tsx | 2 +- .../groups/popover/group-popover.tsx | 2 +- .../components/sidebar-navigation-link.tsx | 79 +++++++++---------- .../src/components/statuses/status-info.tsx | 2 +- packages/pl-fe/src/components/ui/avatar.tsx | 2 +- .../pl-fe/src/components/ui/icon-button.tsx | 4 +- packages/pl-fe/src/components/ui/toast.tsx | 2 +- .../chat-widget/chat-pane-header.tsx | 2 +- .../components/reply-group-indicator.tsx | 2 +- 9 files changed, 46 insertions(+), 51 deletions(-) diff --git a/packages/pl-fe/src/components/groups/group-avatar.tsx b/packages/pl-fe/src/components/groups/group-avatar.tsx index 028a3c116..a2a98cacd 100644 --- a/packages/pl-fe/src/components/groups/group-avatar.tsx +++ b/packages/pl-fe/src/components/groups/group-avatar.tsx @@ -10,7 +10,7 @@ interface IGroupAvatar { withRing?: boolean; } -const GroupAvatar = (props: IGroupAvatar) => { +const GroupAvatar: React.FC = (props) => { const { group, size, withRing = false } = props; const isOwner = group.relationship?.role === GroupRoles.OWNER; diff --git a/packages/pl-fe/src/components/groups/popover/group-popover.tsx b/packages/pl-fe/src/components/groups/popover/group-popover.tsx index 9649e6412..a4117e0d8 100644 --- a/packages/pl-fe/src/components/groups/popover/group-popover.tsx +++ b/packages/pl-fe/src/components/groups/popover/group-popover.tsx @@ -23,7 +23,7 @@ interface IGroupPopoverContainer { group: Group; } -const GroupPopover = (props: IGroupPopoverContainer) => { +const GroupPopover: React.FC = (props) => { const { children, group, isEnabled } = props; const shouldHideAction = !!useMatch({ from: groupTimelineRoute.fullPath, shouldThrow: false }); diff --git a/packages/pl-fe/src/components/sidebar-navigation-link.tsx b/packages/pl-fe/src/components/sidebar-navigation-link.tsx index f1fc2e2a6..b50ee6a58 100644 --- a/packages/pl-fe/src/components/sidebar-navigation-link.tsx +++ b/packages/pl-fe/src/components/sidebar-navigation-link.tsx @@ -21,54 +21,49 @@ interface ISidebarNavigationLink extends Partial { } /** Desktop sidebar navigation link. */ -const SidebarNavigationLink = React.memo( - React.forwardRef( - ( - props: ISidebarNavigationLink, - ref: React.ForwardedRef, - ): React.JSX.Element => { - const { icon, activeIcon, text, to, count, countMax, onClick, ...rest } = props; +const SidebarNavigationLink: React.FC = React.memo( + React.forwardRef((props, ref: React.ForwardedRef): React.JSX.Element => { + const { icon, activeIcon, text, to, count, countMax, onClick, ...rest } = props; - const matchRoute = useMatchRoute(); - const { demetricator } = useSettings(); + const matchRoute = useMatchRoute(); + const { demetricator } = useSettings(); - const LinkComponent = (to === undefined ? 'button' : Link) as typeof Link; + const LinkComponent = (to === undefined ? 'button' : Link) as typeof Link; - const isActive = matchRoute({ to }) !== false; + const isActive = matchRoute({ to }) !== false; - const handleClick: React.EventHandler = (e) => { - if (onClick) { - onClick(e); - e.preventDefault(); - e.stopPropagation(); - } - }; + const handleClick: React.EventHandler = (e) => { + if (onClick) { + onClick(e); + e.preventDefault(); + e.stopPropagation(); + } + }; - return ( -
  • - - - - + return ( +
  • + + + + -

    {text}

    -
    -
  • - ); - }, - ), +

    {text}

    + + + ); + }), (prevProps, nextProps) => prevProps.count === nextProps.count, ); diff --git a/packages/pl-fe/src/components/statuses/status-info.tsx b/packages/pl-fe/src/components/statuses/status-info.tsx index 098919e0c..057094701 100644 --- a/packages/pl-fe/src/components/statuses/status-info.tsx +++ b/packages/pl-fe/src/components/statuses/status-info.tsx @@ -9,7 +9,7 @@ interface IStatusInfo { title?: string; } -const StatusInfo = (props: IStatusInfo) => { +const StatusInfo: React.FC = (props) => { const { avatarSize, icon, text, className, title } = props; const onClick = (event: React.MouseEvent) => { diff --git a/packages/pl-fe/src/components/ui/avatar.tsx b/packages/pl-fe/src/components/ui/avatar.tsx index ccb9a4173..4ece5eea0 100644 --- a/packages/pl-fe/src/components/ui/avatar.tsx +++ b/packages/pl-fe/src/components/ui/avatar.tsx @@ -42,7 +42,7 @@ interface IAvatar extends Pick { +const Avatar: React.FC = (props) => { const intl = useIntl(); const { disableUserProvidedMedia } = useSettings(); diff --git a/packages/pl-fe/src/components/ui/icon-button.tsx b/packages/pl-fe/src/components/ui/icon-button.tsx index 56c5f5dbc..d35c90b79 100644 --- a/packages/pl-fe/src/components/ui/icon-button.tsx +++ b/packages/pl-fe/src/components/ui/icon-button.tsx @@ -20,8 +20,8 @@ interface IIconButton extends React.ButtonHTMLAttributes { } /** A clickable icon. */ -const IconButton = React.forwardRef( - (props: IIconButton, ref: React.ForwardedRef): React.JSX.Element => { +const IconButton: React.FC = React.forwardRef( + (props, ref: React.ForwardedRef): React.JSX.Element => { const { src, className, iconClassName, text, theme = 'seamless', ...filteredProps } = props; const Component = (props.href ? 'a' : 'button') as 'button'; diff --git a/packages/pl-fe/src/components/ui/toast.tsx b/packages/pl-fe/src/components/ui/toast.tsx index 779fe5ecd..f8d2df66f 100644 --- a/packages/pl-fe/src/components/ui/toast.tsx +++ b/packages/pl-fe/src/components/ui/toast.tsx @@ -36,7 +36,7 @@ interface IToast { /** * Customizable Toasts for in-app notifications. */ -const Toast = (props: IToast) => { +const Toast: React.FC = (props) => { const { t, message, type, action, actionLinkOptions, actionLabel, summary } = props; const intl = useIntl(); diff --git a/packages/pl-fe/src/features/chats/components/chat-widget/chat-pane-header.tsx b/packages/pl-fe/src/features/chats/components/chat-widget/chat-pane-header.tsx index 0d2912619..03217555c 100644 --- a/packages/pl-fe/src/features/chats/components/chat-widget/chat-pane-header.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-widget/chat-pane-header.tsx @@ -20,7 +20,7 @@ interface IChatPaneHeader { secondaryActionTitle?: string; } -const ChatPaneHeader = (props: IChatPaneHeader) => { +const ChatPaneHeader: React.FC = (props) => { const { isOpen, isToggleable = true, diff --git a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx index e14d2487b..70cd23d5a 100644 --- a/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx +++ b/packages/pl-fe/src/features/compose/components/reply-group-indicator.tsx @@ -13,7 +13,7 @@ interface IReplyGroupIndicator { composeId: string; } -const ReplyGroupIndicator = (props: IReplyGroupIndicator) => { +const ReplyGroupIndicator: React.FC = (props) => { const { composeId } = props; const getStatus = useCallback(makeGetStatus(), []); From ffb6af3eef2c3ad7248053ce4cffd0fa999d2b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 25 Feb 2026 23:51:40 +0100 Subject: [PATCH 074/264] nicolium: i'm bad at search and replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../plugins/floating-text-format-toolbar-plugin.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx index 61de50137..86bfd4011 100644 --- a/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx +++ b/packages/pl-fe/src/features/compose/editor/plugins/floating-text-format-toolbar-plugin.tsx @@ -406,7 +406,7 @@ const TextFormatFloatingToolbar = ({ }} active={isBold} aria-label={intl.formatMessage(messages.formatBold)} - aria-label={intl.formatMessage(messages.formatBold)} + title={intl.formatMessage(messages.formatBold)} icon={require('@phosphor-icons/core/regular/text-b.svg')} /> From d06266fe043813c5b870a4c0deba6976c602f5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 26 Feb 2026 00:09:17 +0100 Subject: [PATCH 075/264] nicolium: make chat list hotkey-navigable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- .../pl-fe/src/components/ui/icon-button.tsx | 4 +- .../chats/components/chat-list-item.tsx | 276 ++++++++++-------- .../chats/components/chat-list-shoutbox.tsx | 91 +++--- .../features/chats/components/chat-list.tsx | 43 ++- 4 files changed, 245 insertions(+), 169 deletions(-) diff --git a/packages/pl-fe/src/components/ui/icon-button.tsx b/packages/pl-fe/src/components/ui/icon-button.tsx index d35c90b79..56c5f5dbc 100644 --- a/packages/pl-fe/src/components/ui/icon-button.tsx +++ b/packages/pl-fe/src/components/ui/icon-button.tsx @@ -20,8 +20,8 @@ interface IIconButton extends React.ButtonHTMLAttributes { } /** A clickable icon. */ -const IconButton: React.FC = React.forwardRef( - (props, ref: React.ForwardedRef): React.JSX.Element => { +const IconButton = React.forwardRef( + (props: IIconButton, ref: React.ForwardedRef): React.JSX.Element => { const { src, className, iconClassName, text, theme = 'seamless', ...filteredProps } = props; const Component = (props.href ? 'a' : 'button') as 'button'; diff --git a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx index 83e84ad13..b3ac346b8 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-item.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-item.tsx @@ -11,6 +11,7 @@ import IconButton from '@/components/ui/icon-button'; import VerificationBadge from '@/components/verification-badge'; import { useChatContext } from '@/contexts/chat-context'; import Emojify from '@/features/emoji/emojify'; +import { Hotkeys } from '@/features/ui/components/hotkeys'; import { useFeatures } from '@/hooks/use-features'; import { useRelationshipQuery } from '@/queries/accounts/use-relationship'; import { useDeleteChat } from '@/queries/chats'; @@ -34,146 +35,169 @@ const messages = defineMessages({ interface IChatListItem { chat: Chat; onClick: (chat: Chat) => void; + onMoveUp?: (chatId: string) => void; + onMoveDown?: (chatId: string) => void; } -const ChatListItem: React.FC = React.memo(({ chat, onClick }) => { - const { openModal } = useModalsActions(); - const intl = useIntl(); - const features = useFeatures(); - const navigate = useNavigate(); +const ChatListItem: React.FC = React.memo( + ({ chat, onClick, onMoveUp, onMoveDown }) => { + const { openModal } = useModalsActions(); + const intl = useIntl(); + const features = useFeatures(); + const navigate = useNavigate(); - const { isUsingMainChatPage } = useChatContext(); - const deleteChat = useDeleteChat(chat?.id); - const { data: relationship } = useRelationshipQuery(chat?.account.id); + const { isUsingMainChatPage } = useChatContext(); + const deleteChat = useDeleteChat(chat?.id); + const { data: relationship } = useRelationshipQuery(chat?.account.id); - const isBlocked = relationship?.blocked_by && false; - const isBlocking = relationship?.blocking && false; + const isBlocked = relationship?.blocked_by && false; + const isBlocking = relationship?.blocking && false; - const menu = useMemo( - (): Menu => [ - { - text: intl.formatMessage(messages.leaveChat), - action: (event) => { - event.stopPropagation(); + const menu = useMemo( + (): Menu => [ + { + text: intl.formatMessage(messages.leaveChat), + action: (event) => { + event.stopPropagation(); - openModal('CONFIRM', { - heading: intl.formatMessage(messages.leaveHeading), - message: intl.formatMessage(messages.leaveMessage), - confirm: intl.formatMessage(messages.leaveConfirm), - onConfirm: () => { - deleteChat.mutate(undefined, { - onSuccess() { - if (isUsingMainChatPage) { - navigate({ to: '/chats' }); - } - }, - }); - }, - }); + openModal('CONFIRM', { + heading: intl.formatMessage(messages.leaveHeading), + message: intl.formatMessage(messages.leaveMessage), + confirm: intl.formatMessage(messages.leaveConfirm), + onConfirm: () => { + deleteChat.mutate(undefined, { + onSuccess() { + if (isUsingMainChatPage) { + navigate({ to: '/chats' }); + } + }, + }); + }, + }); + }, + icon: require('@phosphor-icons/core/regular/sign-out.svg'), }, - icon: require('@phosphor-icons/core/regular/sign-out.svg'), - }, - ], - [], - ); + ], + [], + ); - const handleKeyDown: React.KeyboardEventHandler = (event) => { - if (event.key === 'Enter' || event.key === ' ') { - onClick(chat); - } - }; - - return ( -
    { + const handleKeyDown: React.KeyboardEventHandler = (event) => { + if (event.key === 'Enter' || event.key === ' ') { onClick(chat); - }} - onKeyDown={handleKeyDown} - className='⁂-chat-list-item' - data-testid='chat-list-item' - tabIndex={0} - > -
    -
    - + } + }; -
    -
    -

    - -

    - {chat.account?.verified && } + const handleMoveUp = () => { + if (onMoveUp) { + onMoveUp(chat.id); + } + }; + + const handleMoveDown = () => { + if (onMoveDown) { + onMoveDown(chat.id); + } + }; + + const handlers = { + moveUp: handleMoveUp, + moveDown: handleMoveDown, + }; + + return ( + { + onClick(chat); + }} + onKeyDown={handleKeyDown} + > +
    +
    +
    + + +
    +
    +

    + +

    + {chat.account?.verified && } +
    + +

    + {isBlocked ? ( + + ) : isBlocking ? ( + + ) : ( + chat.last_message?.content && ( + + ) + )} +

    +
    -

    - {isBlocked ? ( - - ) : isBlocking ? ( - - ) : ( - chat.last_message?.content && ( - - ) +

    + {features.chatsDelete && ( +
    + + + +
    )} -

    + + {chat.last_message && ( + <> + {chat.last_message.unread && ( +
    + )} + + + + )} +
    - -
    - {features.chatsDelete && ( -
    - - - -
    - )} - - {chat.last_message && ( - <> - {chat.last_message.unread && ( -
    - )} - - - - )} -
    -
    -
    - ); -}); +
    + ); + }, +); export { ChatListItem as default }; diff --git a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx index 32e6a063d..df074eae2 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx @@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl'; import { ParsedContent } from '@/components/parsed-content'; import Avatar from '@/components/ui/avatar'; import Emojify from '@/features/emoji/emojify'; +import { Hotkeys } from '@/features/ui/components/hotkeys'; import { useFrontendConfig } from '@/hooks/use-frontend-config'; import { useInstance } from '@/hooks/use-instance'; import { useAccount } from '@/queries/accounts/use-account'; @@ -13,9 +14,11 @@ import type { Chat } from 'pl-api'; interface IChatListShoutbox { onClick: (chat: Chat | 'shoutbox') => void; + onMoveUp?: (chatId: string) => void; + onMoveDown?: (chatId: string) => void; } -const ChatListShoutbox: React.FC = ({ onClick }) => { +const ChatListShoutbox: React.FC = ({ onClick, onMoveUp, onMoveDown }) => { const instance = useInstance(); const { logo } = useFrontendConfig(); const messages = useShoutboxMessages(); @@ -26,53 +29,71 @@ const ChatListShoutbox: React.FC = ({ onClick }) => { } }; + const handleMoveUp = () => { + if (onMoveUp) { + onMoveUp('shoutbox'); + } + }; + + const handleMoveDown = () => { + if (onMoveDown) { + onMoveDown('shoutbox'); + } + }; + + const handlers = { + moveUp: handleMoveUp, + moveDown: handleMoveDown, + }; + const lastMessage = messages.at(-1); const { data: lastMessageAuthor } = useAccount(lastMessage?.author_id); return ( -
    { onClick('shoutbox'); }} onKeyDown={handleKeyDown} - className='⁂-chat-list-item ⁂-chat-list-item--shoutbox' - data-testid='chat-list-item' - tabIndex={0} > -
    - -
    -
    -

    - -

    -
    - - {lastMessage && ( - <> -

    - {lastMessageAuthor && ( - - - {': '} - - )} - +

    +
    + +
    +
    +

    +

    - - )} +
    + + {lastMessage && ( + <> +

    + {lastMessageAuthor && ( + + + {': '} + + )} + +

    + + )} +
    -
    + ); }; diff --git a/packages/pl-fe/src/features/chats/components/chat-list.tsx b/packages/pl-fe/src/features/chats/components/chat-list.tsx index 04d1f9781..9d53c0196 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; -import React, { useCallback, useState } from 'react'; -import { Virtuoso } from 'react-virtuoso'; +import React, { useCallback, useRef, useState } from 'react'; +import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso'; import PullToRefresh from '@/components/pull-to-refresh'; import Spinner from '@/components/ui/spinner'; @@ -8,6 +8,7 @@ import Stack from '@/components/ui/stack'; import PlaceholderChat from '@/features/placeholder/components/placeholder-chat'; import { useChats } from '@/queries/chats'; import { useShoutboxIsLoading } from '@/stores/shoutbox'; +import { selectChild } from '@/utils/scroll-utils'; import ChatListItem from './chat-list-item'; import ChatListShoutbox from './chat-list-shoutbox'; @@ -20,6 +21,8 @@ interface IChatList { } const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) => { + const node = useRef(null); + const showShoutbox = !useShoutboxIsLoading(); const { @@ -41,20 +44,47 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) const handleRefresh = () => refetch(); + const getCurrentIndex = (id: string): number => + allChats?.findIndex((key) => (key === 'shoutbox' ? key : key.id) === id) ?? -1; + + const handleMoveUp = (chatId: string) => { + const elementIndex = getCurrentIndex(chatId) - 1; + selectChild(elementIndex, node, document.querySelector('.⁂-chat-widget__list') ?? undefined); + }; + + const handleMoveDown = (chatId: string) => { + const elementIndex = getCurrentIndex(chatId) + 1; + selectChild( + elementIndex, + node, + document.querySelector('.⁂-chat-widget__list') ?? undefined, + allChats?.length, + ); + }; + const renderChatListItem = useCallback( (_index: number, chat: Chat | 'shoutbox') => { if (chat === 'shoutbox') { return (
    - +
    ); } return ( -
    - -
    + ); }, [onClickChat], @@ -83,6 +113,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) })} > { setNearTop(atTop); }} From 08d5bc770e9b61d7d10dd6a76d0022cd1a008bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Thu, 26 Feb 2026 00:23:18 +0100 Subject: [PATCH 076/264] nicolium: make caht textarea less ugly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/features/chats/components/chat-textarea.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/pl-fe/src/features/chats/components/chat-textarea.tsx b/packages/pl-fe/src/features/chats/components/chat-textarea.tsx index 44c4c4993..ca58ba176 100644 --- a/packages/pl-fe/src/features/chats/components/chat-textarea.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-textarea.tsx @@ -41,7 +41,7 @@ const ChatTextarea = React.forwardRef( )} -